rims 0.2.2 → 0.2.3
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/.gitignore +19 -17
- data/ChangeLog +36 -0
- data/Rakefile +70 -1
- data/lib/rims.rb +3 -5
- data/lib/rims/auth.rb +5 -7
- data/lib/rims/cmd.rb +758 -188
- data/lib/rims/error.rb +8 -10
- data/lib/rims/kvs.rb +4 -0
- data/lib/rims/lock.rb +4 -1
- data/lib/rims/protocol.rb +7 -1
- data/lib/rims/protocol/decoder.rb +24 -7
- data/lib/rims/protocol/parser.rb +1 -1
- data/lib/rims/service.rb +953 -0
- data/lib/rims/version.rb +1 -1
- data/rims.gemspec +2 -0
- data/test/cmd/test_command.rb +999 -0
- data/test/test_auth.rb +3 -1
- data/test/test_cmd.rb +36 -0
- data/test/test_error.rb +22 -78
- data/test/test_protocol_auth.rb +3 -1
- data/test/test_protocol_decoder.rb +6 -3
- data/test/test_service.rb +1120 -0
- metadata +38 -11
- data/lib/rims/daemon.rb +0 -338
- data/lib/rims/server.rb +0 -567
- data/test/test_config.rb +0 -533
- data/test/test_daemon_status_file.rb +0 -169
- data/test/test_daemon_waitpid.rb +0 -72
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rims
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TOKI Yoshinori
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: riser
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.7
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logger-joint
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: bundler
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,7 +118,6 @@ files:
|
|
90
118
|
- lib/rims/channel.rb
|
91
119
|
- lib/rims/cksum_kvs.rb
|
92
120
|
- lib/rims/cmd.rb
|
93
|
-
- lib/rims/daemon.rb
|
94
121
|
- lib/rims/db.rb
|
95
122
|
- lib/rims/error.rb
|
96
123
|
- lib/rims/gdbm_kvs.rb
|
@@ -104,17 +131,16 @@ files:
|
|
104
131
|
- lib/rims/protocol/decoder.rb
|
105
132
|
- lib/rims/protocol/parser.rb
|
106
133
|
- lib/rims/rfc822.rb
|
107
|
-
- lib/rims/
|
134
|
+
- lib/rims/service.rb
|
108
135
|
- lib/rims/test.rb
|
109
136
|
- lib/rims/version.rb
|
110
137
|
- load_test/Rakefile
|
111
138
|
- rims.gemspec
|
139
|
+
- test/cmd/test_command.rb
|
112
140
|
- test/test_auth.rb
|
113
141
|
- test/test_channel.rb
|
114
142
|
- test/test_cksum_kvs.rb
|
115
|
-
- test/
|
116
|
-
- test/test_daemon_status_file.rb
|
117
|
-
- test/test_daemon_waitpid.rb
|
143
|
+
- test/test_cmd.rb
|
118
144
|
- test/test_db.rb
|
119
145
|
- test/test_db_recovery.rb
|
120
146
|
- test/test_error.rb
|
@@ -130,6 +156,7 @@ files:
|
|
130
156
|
- test/test_protocol_request.rb
|
131
157
|
- test/test_protocol_search.rb
|
132
158
|
- test/test_rfc822.rb
|
159
|
+
- test/test_service.rb
|
133
160
|
homepage: https://github.com/y10k/rims
|
134
161
|
licenses:
|
135
162
|
- MIT
|
@@ -149,17 +176,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
176
|
- !ruby/object:Gem::Version
|
150
177
|
version: '0'
|
151
178
|
requirements: []
|
152
|
-
rubygems_version: 3.0.
|
179
|
+
rubygems_version: 3.0.3
|
153
180
|
signing_key:
|
154
181
|
specification_version: 4
|
155
182
|
summary: RIMS is Ruby IMap Server
|
156
183
|
test_files:
|
184
|
+
- test/cmd/test_command.rb
|
157
185
|
- test/test_auth.rb
|
158
186
|
- test/test_channel.rb
|
159
187
|
- test/test_cksum_kvs.rb
|
160
|
-
- test/
|
161
|
-
- test/test_daemon_status_file.rb
|
162
|
-
- test/test_daemon_waitpid.rb
|
188
|
+
- test/test_cmd.rb
|
163
189
|
- test/test_db.rb
|
164
190
|
- test/test_db_recovery.rb
|
165
191
|
- test/test_error.rb
|
@@ -175,3 +201,4 @@ test_files:
|
|
175
201
|
- test/test_protocol_request.rb
|
176
202
|
- test/test_protocol_search.rb
|
177
203
|
- test/test_rfc822.rb
|
204
|
+
- test/test_service.rb
|
data/lib/rims/daemon.rb
DELETED
@@ -1,338 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
require 'logger'
|
4
|
-
require 'yaml'
|
5
|
-
|
6
|
-
module RIMS
|
7
|
-
class Daemon
|
8
|
-
class ExclusiveStatusFile
|
9
|
-
def initialize(filename)
|
10
|
-
@filename = filename
|
11
|
-
@file = nil
|
12
|
-
@is_locked = false
|
13
|
-
end
|
14
|
-
|
15
|
-
def open
|
16
|
-
if (block_given?) then
|
17
|
-
open
|
18
|
-
begin
|
19
|
-
r = yield
|
20
|
-
ensure
|
21
|
-
close
|
22
|
-
end
|
23
|
-
return r
|
24
|
-
end
|
25
|
-
|
26
|
-
begin
|
27
|
-
@file = File.open(@filename, File::WRONLY | File::CREAT, 0640)
|
28
|
-
rescue SystemCallError
|
29
|
-
@fiile = File.open(@filename, File::WRONLY)
|
30
|
-
end
|
31
|
-
|
32
|
-
self
|
33
|
-
end
|
34
|
-
|
35
|
-
def close
|
36
|
-
@file.close
|
37
|
-
self
|
38
|
-
end
|
39
|
-
|
40
|
-
def locked?
|
41
|
-
@is_locked
|
42
|
-
end
|
43
|
-
|
44
|
-
def should_be_locked
|
45
|
-
unless (locked?) then
|
46
|
-
raise "not locked: #{@filename}"
|
47
|
-
end
|
48
|
-
self
|
49
|
-
end
|
50
|
-
|
51
|
-
def should_not_be_locked
|
52
|
-
if (locked?) then
|
53
|
-
raise "already locked: #{@filename}"
|
54
|
-
end
|
55
|
-
self
|
56
|
-
end
|
57
|
-
|
58
|
-
def lock
|
59
|
-
should_not_be_locked
|
60
|
-
unless (@file.flock(File::LOCK_EX | File::LOCK_NB)) then
|
61
|
-
raise "locked by another process: #{@filename}"
|
62
|
-
end
|
63
|
-
@is_locked = true
|
64
|
-
self
|
65
|
-
end
|
66
|
-
|
67
|
-
def unlock
|
68
|
-
should_be_locked
|
69
|
-
@file.flock(File::LOCK_UN)
|
70
|
-
@is_locked = false
|
71
|
-
self
|
72
|
-
end
|
73
|
-
|
74
|
-
def synchronize
|
75
|
-
lock
|
76
|
-
begin
|
77
|
-
yield
|
78
|
-
ensure
|
79
|
-
unlock
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def write(text)
|
84
|
-
should_be_locked
|
85
|
-
|
86
|
-
@file.truncate(0)
|
87
|
-
@file.syswrite(text)
|
88
|
-
|
89
|
-
self
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
class ReadableStatusFile
|
94
|
-
def initialize(filename)
|
95
|
-
@filename = filename
|
96
|
-
@file = nil
|
97
|
-
end
|
98
|
-
|
99
|
-
def open
|
100
|
-
if (block_given?) then
|
101
|
-
open
|
102
|
-
begin
|
103
|
-
r = yield
|
104
|
-
ensure
|
105
|
-
close
|
106
|
-
end
|
107
|
-
return r
|
108
|
-
end
|
109
|
-
|
110
|
-
@file = File.open(@filename, File::RDONLY)
|
111
|
-
|
112
|
-
self
|
113
|
-
end
|
114
|
-
|
115
|
-
def close
|
116
|
-
@file.close
|
117
|
-
self
|
118
|
-
end
|
119
|
-
|
120
|
-
def locked?
|
121
|
-
if (@file.flock(File::LOCK_EX | File::LOCK_NB)) then
|
122
|
-
@file.flock(File::LOCK_UN)
|
123
|
-
false
|
124
|
-
else
|
125
|
-
true
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def should_be_locked
|
130
|
-
unless (locked?) then
|
131
|
-
raise "not locked: #{@filename}"
|
132
|
-
end
|
133
|
-
self
|
134
|
-
end
|
135
|
-
|
136
|
-
def read
|
137
|
-
should_be_locked
|
138
|
-
@file.seek(0)
|
139
|
-
@file.read
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def self.new_status_file(filename, exclusive: false)
|
144
|
-
if (exclusive) then
|
145
|
-
ExclusiveStatusFile.new(filename)
|
146
|
-
else
|
147
|
-
ReadableStatusFile.new(filename)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def self.make_stat_file_path(base_dir)
|
152
|
-
File.join(base_dir, 'status')
|
153
|
-
end
|
154
|
-
|
155
|
-
RELOAD_SIGNAL_LIST = %w[ HUP ]
|
156
|
-
RESTART_SIGNAL_LIST = %w[ USR1 ]
|
157
|
-
STOP_SIGNAL_LIST = %w[ TERM INT ]
|
158
|
-
|
159
|
-
RELOAD_SIGNAL = RELOAD_SIGNAL_LIST[0]
|
160
|
-
RESTART_SIGNAL = RESTART_SIGNAL_LIST[0]
|
161
|
-
STOP_SIGNAL = STOP_SIGNAL_LIST[0]
|
162
|
-
|
163
|
-
SERVER_RESTART_INTERVAL_SECONDS = 5
|
164
|
-
|
165
|
-
class ChildProcess
|
166
|
-
# return self if child process has been existed.
|
167
|
-
# return nil if no child process.
|
168
|
-
def self.cleanup_terminated_process(logger)
|
169
|
-
begin
|
170
|
-
while (pid = Process.waitpid(-1))
|
171
|
-
if ($?.exitstatus != 0) then
|
172
|
-
logger.warn("aborted child process: #{pid} (#{$?.exitstatus})")
|
173
|
-
end
|
174
|
-
yield(pid) if block_given?
|
175
|
-
end
|
176
|
-
rescue Errno::ECHILD
|
177
|
-
return
|
178
|
-
end
|
179
|
-
|
180
|
-
self
|
181
|
-
end
|
182
|
-
|
183
|
-
def initialize(logger=Logger.new(STDOUT))
|
184
|
-
@logger = logger
|
185
|
-
@pid = run{ yield }
|
186
|
-
end
|
187
|
-
|
188
|
-
attr_reader :pid
|
189
|
-
|
190
|
-
def run
|
191
|
-
begin
|
192
|
-
pipe_in, pipe_out = IO.pipe
|
193
|
-
pid = Process.fork{
|
194
|
-
@logger.close
|
195
|
-
pipe_in.close
|
196
|
-
|
197
|
-
status_code = catch(:rims_daemon_child_process_stop) {
|
198
|
-
for sig_name in RELOAD_SIGNAL_LIST + RESTART_SIGNAL_LIST
|
199
|
-
Signal.trap(sig_name, :DEFAULT)
|
200
|
-
end
|
201
|
-
for sig_name in STOP_SIGNAL_LIST
|
202
|
-
Signal.trap(sig_name) { throw(:rims_daemon_child_process_stop, 0) }
|
203
|
-
end
|
204
|
-
|
205
|
-
pipe_out.puts("child process (pid: #{$$}) is ready to go.")
|
206
|
-
pipe_out.close
|
207
|
-
|
208
|
-
yield
|
209
|
-
}
|
210
|
-
exit!(status_code)
|
211
|
-
}
|
212
|
-
rescue
|
213
|
-
@logger.error("failed to fork new child process: #{$!}")
|
214
|
-
return
|
215
|
-
end
|
216
|
-
|
217
|
-
begin
|
218
|
-
pipe_out.close
|
219
|
-
s = pipe_in.gets
|
220
|
-
@logger.info("[child process message] #{s}") if $DEBUG
|
221
|
-
pipe_in.close
|
222
|
-
rescue
|
223
|
-
@logger.error("failed to start new child process: #{$!}")
|
224
|
-
begin
|
225
|
-
Process.kill(STOP_SIGNAL, pid)
|
226
|
-
rescue SystemCallError
|
227
|
-
@logger.warn("failed to kill abnormal child process: #{$!}")
|
228
|
-
end
|
229
|
-
return
|
230
|
-
end
|
231
|
-
|
232
|
-
pid
|
233
|
-
end
|
234
|
-
private :run
|
235
|
-
|
236
|
-
def forked?
|
237
|
-
@pid != nil
|
238
|
-
end
|
239
|
-
|
240
|
-
def terminate
|
241
|
-
begin
|
242
|
-
Process.kill(STOP_SIGNAL, @pid)
|
243
|
-
rescue SystemCallError
|
244
|
-
@logger.warn("failed to terminate child process: #{@pid}")
|
245
|
-
end
|
246
|
-
|
247
|
-
nil
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
def initialize(stat_file_path, logger=Logger.new(STDOUT), server_options: [])
|
252
|
-
@stat_file = self.class.new_status_file(stat_file_path, exclusive: true)
|
253
|
-
@logger = logger
|
254
|
-
@server_options = server_options
|
255
|
-
@server_running = true
|
256
|
-
@server_process = nil
|
257
|
-
end
|
258
|
-
|
259
|
-
def new_server_process
|
260
|
-
ChildProcess.new(@logger) { Cmd.run_cmd(%w[ server ] + @server_options) }
|
261
|
-
end
|
262
|
-
private :new_server_process
|
263
|
-
|
264
|
-
def run
|
265
|
-
@stat_file.open{
|
266
|
-
@stat_file.synchronize{
|
267
|
-
@stat_file.write({ 'pid' => $$ }.to_yaml)
|
268
|
-
begin
|
269
|
-
@logger.info('start daemon.')
|
270
|
-
loop do
|
271
|
-
break unless @server_running
|
272
|
-
|
273
|
-
unless (@server_process && @server_process.forked?) then
|
274
|
-
start_time = Time.now
|
275
|
-
@server_process = new_server_process
|
276
|
-
@logger.info("run server process: #{@server_process.pid}")
|
277
|
-
end
|
278
|
-
|
279
|
-
break unless @server_running
|
280
|
-
|
281
|
-
ChildProcess.cleanup_terminated_process(@logger) do |pid|
|
282
|
-
if (@server_process.pid == pid) then
|
283
|
-
@server_process = nil
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
break unless @server_running
|
288
|
-
|
289
|
-
elapsed_seconds = Time.now - start_time
|
290
|
-
the_rest_in_interval_seconds = SERVER_RESTART_INTERVAL_SECONDS - elapsed_seconds
|
291
|
-
sleep(the_rest_in_interval_seconds) if (the_rest_in_interval_seconds > 0)
|
292
|
-
end
|
293
|
-
ensure
|
294
|
-
if (@server_process && @server_process.forked?) then
|
295
|
-
@server_process.terminate
|
296
|
-
ChildProcess.cleanup_terminated_process(@logger)
|
297
|
-
end
|
298
|
-
@logger.info('stop daemon.')
|
299
|
-
end
|
300
|
-
}
|
301
|
-
}
|
302
|
-
|
303
|
-
self
|
304
|
-
end
|
305
|
-
|
306
|
-
# signal trap hook.
|
307
|
-
# this method is not true reload.
|
308
|
-
def reload_server
|
309
|
-
restart_server
|
310
|
-
end
|
311
|
-
|
312
|
-
# signal trap hook.
|
313
|
-
def restart_server
|
314
|
-
@stat_file.should_be_locked
|
315
|
-
if (@server_process && @server_process.forked?) then
|
316
|
-
@server_process.terminate
|
317
|
-
end
|
318
|
-
|
319
|
-
self
|
320
|
-
end
|
321
|
-
|
322
|
-
# signal trap hook.
|
323
|
-
def stop_server
|
324
|
-
@stat_file.should_be_locked
|
325
|
-
@server_running = false
|
326
|
-
if (@server_process && @server_process.forked?) then
|
327
|
-
@server_process.terminate
|
328
|
-
end
|
329
|
-
|
330
|
-
self
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
# Local Variables:
|
336
|
-
# mode: Ruby
|
337
|
-
# indent-tabs-mode: nil
|
338
|
-
# End:
|
data/lib/rims/server.rb
DELETED
@@ -1,567 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
require 'etc'
|
4
|
-
require 'forwardable'
|
5
|
-
require 'logger'
|
6
|
-
require 'pathname'
|
7
|
-
require 'socket'
|
8
|
-
require 'yaml'
|
9
|
-
|
10
|
-
module RIMS
|
11
|
-
class Multiplexor
|
12
|
-
def initialize
|
13
|
-
@obj_list = []
|
14
|
-
end
|
15
|
-
|
16
|
-
def add(object)
|
17
|
-
@obj_list << object
|
18
|
-
self
|
19
|
-
end
|
20
|
-
|
21
|
-
def method_missing(id, *args)
|
22
|
-
for object in @obj_list
|
23
|
-
r = object.__send__(id, *args)
|
24
|
-
end
|
25
|
-
r
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
class Config
|
30
|
-
extend Forwardable
|
31
|
-
|
32
|
-
def self.relative_path?(path)
|
33
|
-
Pathname.new(path).relative?
|
34
|
-
end
|
35
|
-
|
36
|
-
def_delegator 'self.class', :relative_path?
|
37
|
-
private :relative_path?
|
38
|
-
|
39
|
-
def self.load_plug_in_configuration(base_dir, config)
|
40
|
-
if ((config.key? 'configuration') && (config.key? 'configuration_file')) then
|
41
|
-
raise 'configuration conflict: configuration, configuraion_file'
|
42
|
-
end
|
43
|
-
|
44
|
-
if (config.key? 'configuration') then
|
45
|
-
config['configuration']
|
46
|
-
elsif (config.key? 'configuration_file') then
|
47
|
-
config_file = config['configuration_file']
|
48
|
-
if (relative_path? config_file) then
|
49
|
-
config_path = File.join(base_dir, config_file)
|
50
|
-
else
|
51
|
-
config_path = config_file
|
52
|
-
end
|
53
|
-
YAML.load_file(config_path)
|
54
|
-
else
|
55
|
-
{}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def initialize
|
60
|
-
@config = {}
|
61
|
-
end
|
62
|
-
|
63
|
-
def load(config)
|
64
|
-
@config.update(config)
|
65
|
-
self
|
66
|
-
end
|
67
|
-
|
68
|
-
def load_config_yaml(path)
|
69
|
-
load_config_from_base_dir(YAML.load_file(path), File.dirname(path))
|
70
|
-
end
|
71
|
-
|
72
|
-
def load_config_from_base_dir(config, base_dir)
|
73
|
-
@config[:base_dir] = base_dir
|
74
|
-
for name, value in config
|
75
|
-
case (key_sym = name.to_sym)
|
76
|
-
when :base_dir
|
77
|
-
if (relative_path? value) then
|
78
|
-
@config[:base_dir] = File.join(base_dir, value)
|
79
|
-
else
|
80
|
-
@config[:base_dir] = value
|
81
|
-
end
|
82
|
-
else
|
83
|
-
@config[key_sym] = value
|
84
|
-
end
|
85
|
-
end
|
86
|
-
self
|
87
|
-
end
|
88
|
-
|
89
|
-
# configuration entries.
|
90
|
-
# * <tt>:base_dir</tt>
|
91
|
-
#
|
92
|
-
def base_dir
|
93
|
-
@config[:base_dir] or raise 'not defined configuration entry: base_dir'
|
94
|
-
end
|
95
|
-
|
96
|
-
def through_server_params
|
97
|
-
params = @config.dup
|
98
|
-
params.delete(:base_dir)
|
99
|
-
params
|
100
|
-
end
|
101
|
-
|
102
|
-
def setup_backward_compatibility
|
103
|
-
[ [ :imap_host, :ip_addr ],
|
104
|
-
[ :imap_port, :ip_port ]
|
105
|
-
].each do |new_namme, old_name|
|
106
|
-
unless (@config.key? new_namme) then
|
107
|
-
if (@config.key? old_name) then
|
108
|
-
warn("warning: `#{old_name}' is obsoleted server configuration parameter and should be replaced to new parameter of `#{new_namme}'.")
|
109
|
-
@config[new_namme] = @config.delete(old_name)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
self
|
115
|
-
end
|
116
|
-
|
117
|
-
# configuration entry.
|
118
|
-
# * <tt>load_libraries</tt>
|
119
|
-
def setup_load_libraries
|
120
|
-
lib_list = @config.delete(:load_libraries) || []
|
121
|
-
for lib in lib_list
|
122
|
-
require(lib)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# configuration entries.
|
127
|
-
# * <tt>:log_file</tt>
|
128
|
-
# * <tt>:log_level</tt>
|
129
|
-
# * <tt>:log_shift_age</tt>
|
130
|
-
# * <tt>:log_shift_size</tt>
|
131
|
-
# * <tt>:log_stdout</tt>
|
132
|
-
#
|
133
|
-
def logging_params
|
134
|
-
log_file = @config.delete(:log_file) || Server::DEFAULT[:log_file]
|
135
|
-
if (relative_path? log_file) then
|
136
|
-
log_file_path = File.join(base_dir, log_file)
|
137
|
-
else
|
138
|
-
log_file_path = log_file
|
139
|
-
end
|
140
|
-
|
141
|
-
log_level = @config.delete(:log_level) || Server::DEFAULT[:log_level]
|
142
|
-
log_level = log_level.upcase
|
143
|
-
case (log_level)
|
144
|
-
when 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'
|
145
|
-
log_level = Logger.const_get(log_level)
|
146
|
-
else
|
147
|
-
raise "unknown log level of logfile: #{log_level}"
|
148
|
-
end
|
149
|
-
|
150
|
-
log_stdout = @config.delete(:log_stdout) || Server::DEFAULT[:log_stdout]
|
151
|
-
log_stdout = log_stdout.upcase
|
152
|
-
case (log_stdout)
|
153
|
-
when 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'
|
154
|
-
log_stdout = Logger.const_get(log_stdout)
|
155
|
-
when 'QUIET'
|
156
|
-
log_stdout = nil
|
157
|
-
else
|
158
|
-
raise "unknown log level of stdout: #{log_stdout}"
|
159
|
-
end
|
160
|
-
|
161
|
-
log_opt_args = []
|
162
|
-
if (@config.key? :log_shift_age) then
|
163
|
-
log_opt_args << @config.delete(:log_shift_age)
|
164
|
-
log_opt_args << @config.delete(:log_shift_size) if (@config.key? :log_shift_size)
|
165
|
-
else
|
166
|
-
log_opt_args << 1 << @config.delete(:log_shift_size) if (@config.key? :log_shift_size)
|
167
|
-
end
|
168
|
-
|
169
|
-
{ log_file: log_file_path,
|
170
|
-
log_level: log_level,
|
171
|
-
log_opt_args: log_opt_args,
|
172
|
-
log_stdout: log_stdout
|
173
|
-
}
|
174
|
-
end
|
175
|
-
|
176
|
-
def build_logger
|
177
|
-
c = logging_params
|
178
|
-
logger = Multiplexor.new
|
179
|
-
|
180
|
-
if (c[:log_stdout]) then
|
181
|
-
stdout_logger = Logger.new(STDOUT)
|
182
|
-
stdout_logger.level = c[:log_stdout]
|
183
|
-
logger.add(stdout_logger)
|
184
|
-
end
|
185
|
-
|
186
|
-
file_logger = Logger.new(c[:log_file], *c[:log_opt_args])
|
187
|
-
file_logger.level = c[:log_level]
|
188
|
-
logger.add(file_logger)
|
189
|
-
|
190
|
-
logger
|
191
|
-
end
|
192
|
-
|
193
|
-
def key_value_store_params(db_type)
|
194
|
-
kvs_conf = @config.delete(db_type) || {}
|
195
|
-
key_value_store_type = @config.delete(:key_value_store_type) # for backward compatibility
|
196
|
-
use_key_value_store_checksum = @config.delete(:use_key_value_store_checksum) # for backward compatibility
|
197
|
-
|
198
|
-
kvs_type = kvs_conf['plug_in']
|
199
|
-
kvs_type ||= key_value_store_type
|
200
|
-
if (kvs_type) then
|
201
|
-
origin_key_value_store = KeyValueStore::FactoryBuilder.get_plug_in(kvs_type)
|
202
|
-
else
|
203
|
-
origin_key_value_store = Server::DEFAULT[:key_value_store]
|
204
|
-
end
|
205
|
-
origin_config = self.class.load_plug_in_configuration(base_dir, kvs_conf)
|
206
|
-
|
207
|
-
if (kvs_conf.key? 'use_checksum') then
|
208
|
-
use_checksum = kvs_conf['use_checksum']
|
209
|
-
elsif (! use_key_value_store_checksum.nil?) then
|
210
|
-
use_checksum = use_key_value_store_checksum
|
211
|
-
else
|
212
|
-
use_checksum = Server::DEFAULT[:use_key_value_store_checksum]
|
213
|
-
end
|
214
|
-
|
215
|
-
middleware_key_value_store_list = []
|
216
|
-
if (use_checksum) then
|
217
|
-
middleware_key_value_store_list << Checksum_KeyValueStore
|
218
|
-
end
|
219
|
-
|
220
|
-
{ origin_key_value_store: origin_key_value_store,
|
221
|
-
origin_config: origin_config,
|
222
|
-
middleware_key_value_store_list: middleware_key_value_store_list
|
223
|
-
}
|
224
|
-
end
|
225
|
-
|
226
|
-
def build_key_value_store_factory(db_type)
|
227
|
-
c = key_value_store_params(db_type)
|
228
|
-
builder = KeyValueStore::FactoryBuilder.new
|
229
|
-
builder.open{|name| c[:origin_key_value_store].open_with_conf(name, c[:origin_config]) }
|
230
|
-
for middleware_key_value_store in c[:middleware_key_value_store_list]
|
231
|
-
builder.use(middleware_key_value_store)
|
232
|
-
end
|
233
|
-
builder.factory
|
234
|
-
end
|
235
|
-
|
236
|
-
class << self
|
237
|
-
def mkdir_from_base_dir(base_dir, path_name_list)
|
238
|
-
unless (File.directory? base_dir) then
|
239
|
-
raise "not found a base directory: #{base_dir}"
|
240
|
-
end
|
241
|
-
|
242
|
-
mkdir_count = 0
|
243
|
-
make_path_list = [ base_dir ]
|
244
|
-
|
245
|
-
for path_name in path_name_list
|
246
|
-
make_path_list << path_name
|
247
|
-
make_path = File.join(*make_path_list)
|
248
|
-
begin
|
249
|
-
Dir.mkdir(make_path)
|
250
|
-
mkdir_count += 1
|
251
|
-
rescue Errno::EEXIST
|
252
|
-
unless (File.directory? make_path) then
|
253
|
-
raise "not a directory: #{make_path}"
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
make_path if (mkdir_count > 0)
|
259
|
-
end
|
260
|
-
|
261
|
-
def make_key_value_store_path_name_list(mailbox_data_structure_version, unique_user_id, db_name: nil)
|
262
|
-
if (mailbox_data_structure_version.empty?) then
|
263
|
-
raise ArgumentError, 'too short mailbox data structure version.'
|
264
|
-
end
|
265
|
-
if (unique_user_id.length <= 2) then
|
266
|
-
raise ArgumentError, 'too short unique user ID.'
|
267
|
-
end
|
268
|
-
|
269
|
-
bucket_dir_name = unique_user_id[0..1]
|
270
|
-
store_dir_name = unique_user_id[2..-1]
|
271
|
-
path_name_list = [ mailbox_data_structure_version, bucket_dir_name, store_dir_name ]
|
272
|
-
path_name_list << db_name if db_name
|
273
|
-
|
274
|
-
path_name_list
|
275
|
-
end
|
276
|
-
|
277
|
-
def make_key_value_store_path_from_base_dir(base_dir, mailbox_data_structure_version, unique_user_id, db_name: nil)
|
278
|
-
path_name_list = [ base_dir ]
|
279
|
-
path_name_list += make_key_value_store_path_name_list(mailbox_data_structure_version, unique_user_id, db_name: db_name)
|
280
|
-
File.join(*path_name_list)
|
281
|
-
end
|
282
|
-
|
283
|
-
def make_key_value_store_parent_dir_from_base_dir(base_dir, mailbox_data_structure_version, unique_user_id)
|
284
|
-
mkdir_from_base_dir(base_dir, make_key_value_store_path_name_list(mailbox_data_structure_version, unique_user_id))
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
def make_key_value_store_path_from_base_dir(mailbox_data_structure_version, unique_user_id, db_name: nil)
|
289
|
-
self.class.make_key_value_store_path_from_base_dir(base_dir, mailbox_data_structure_version, unique_user_id, db_name: db_name)
|
290
|
-
end
|
291
|
-
|
292
|
-
def make_key_value_store_parent_dir_from_base_dir(mailbox_data_structure_version, unique_user_id)
|
293
|
-
self.class.make_key_value_store_parent_dir_from_base_dir(base_dir, mailbox_data_structure_version, unique_user_id)
|
294
|
-
end
|
295
|
-
|
296
|
-
# configuration entries.
|
297
|
-
# * <tt>:hostname</tt>
|
298
|
-
# * <tt>:username</tt>
|
299
|
-
# * <tt>:password</tt>
|
300
|
-
# * <tt>:user_list => [ { 'user' => 'username 1', 'pass' => 'password 1'}, { 'user' => 'username 2', 'pass' => 'password 2' } ]</tt>
|
301
|
-
# * <tt>:authentication</tt>
|
302
|
-
#
|
303
|
-
def build_authentication
|
304
|
-
hostname = @config.delete(:hostname) || Socket.gethostname
|
305
|
-
auth = Authentication.new(hostname: hostname)
|
306
|
-
|
307
|
-
user_list = []
|
308
|
-
if (username = @config.delete(:username)) then
|
309
|
-
password = @config.delete(:password) or raise 'not defined configuration entry: password'
|
310
|
-
user_list << { 'user' => username, 'pass' => password }
|
311
|
-
end
|
312
|
-
if (@config.key? :user_list) then
|
313
|
-
user_list += @config.delete(:user_list)
|
314
|
-
end
|
315
|
-
for user_entry in user_list
|
316
|
-
auth.entry(user_entry['user'], user_entry['pass'])
|
317
|
-
end
|
318
|
-
|
319
|
-
if (auth_plug_in_list = @config.delete(:authentication)) then
|
320
|
-
for auth_plug_in_entry in auth_plug_in_list
|
321
|
-
name = auth_plug_in_entry['plug_in'] or raise 'undefined plug-in name.'
|
322
|
-
config = self.class.load_plug_in_configuration(base_dir, auth_plug_in_entry)
|
323
|
-
passwd_src = Authentication.get_plug_in(name, config)
|
324
|
-
auth.add_plug_in(passwd_src)
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
auth
|
329
|
-
end
|
330
|
-
|
331
|
-
def privilege_name2id(name)
|
332
|
-
case (name)
|
333
|
-
when Integer
|
334
|
-
name
|
335
|
-
when String
|
336
|
-
begin
|
337
|
-
yield(name)
|
338
|
-
rescue
|
339
|
-
if (name =~ /\A\d+\z/) then
|
340
|
-
name.to_i
|
341
|
-
else
|
342
|
-
raise
|
343
|
-
end
|
344
|
-
end
|
345
|
-
else
|
346
|
-
raise TypeError, "not a process privilege name: #{name}"
|
347
|
-
end
|
348
|
-
end
|
349
|
-
private :privilege_name2id
|
350
|
-
|
351
|
-
# configuration entries.
|
352
|
-
# * <tt>:process_privilege_user</tt>
|
353
|
-
# * <tt>:process_privilege_group</tt>
|
354
|
-
#
|
355
|
-
def setup_privilege_params
|
356
|
-
user = @config.delete(:process_privilege_user) || Server::DEFAULT[:process_privilege_uid]
|
357
|
-
group = @config.delete(:process_privilege_group) || Server::DEFAULT[:process_privilege_gid]
|
358
|
-
|
359
|
-
@config[:process_privilege_uid] = privilege_name2id(user) {|name| Etc.getpwnam(name).uid }
|
360
|
-
@config[:process_privilege_gid] = privilege_name2id(group) {|name| Etc.getgrnam(name).gid }
|
361
|
-
|
362
|
-
self
|
363
|
-
end
|
364
|
-
|
365
|
-
def build_server
|
366
|
-
setup_backward_compatibility
|
367
|
-
|
368
|
-
setup_load_libraries
|
369
|
-
logger = build_logger
|
370
|
-
meta_kvs_factory = build_key_value_store_factory(:meta_key_value_store)
|
371
|
-
text_kvs_factory = build_key_value_store_factory(:text_key_value_store)
|
372
|
-
auth = build_authentication
|
373
|
-
setup_privilege_params
|
374
|
-
|
375
|
-
make_parent_dir_and_logging = proc{|mailbox_data_structure_version, unique_user_id|
|
376
|
-
if (make_dir_path = make_key_value_store_parent_dir_from_base_dir(mailbox_data_structure_version, unique_user_id)) then
|
377
|
-
logger.debug("make a directory: #{make_dir_path}") if logger.debug?
|
378
|
-
end
|
379
|
-
}
|
380
|
-
|
381
|
-
Server.new(kvs_meta_open: proc{|mailbox_data_structure_version, unique_user_id, db_name|
|
382
|
-
make_parent_dir_and_logging.call(mailbox_data_structure_version, unique_user_id)
|
383
|
-
kvs_path = make_key_value_store_path_from_base_dir(mailbox_data_structure_version, unique_user_id, db_name: db_name)
|
384
|
-
logger.debug("meta data key-value store path: #{kvs_path}") if logger.debug?
|
385
|
-
meta_kvs_factory.call(kvs_path)
|
386
|
-
},
|
387
|
-
kvs_text_open: proc{|mailbox_data_structure_version, unique_user_id, db_name|
|
388
|
-
make_parent_dir_and_logging.call(mailbox_data_structure_version, unique_user_id)
|
389
|
-
kvs_path = make_key_value_store_path_from_base_dir(mailbox_data_structure_version, unique_user_id, db_name: db_name)
|
390
|
-
logger.debug("message data key-value store path: #{kvs_path}") if logger.debug?
|
391
|
-
text_kvs_factory.call(kvs_path)
|
392
|
-
},
|
393
|
-
authentication: auth,
|
394
|
-
logger: logger,
|
395
|
-
**through_server_params)
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
class BufferedWriter
|
400
|
-
def initialize(output, buffer_limit=1024*16)
|
401
|
-
@output = output
|
402
|
-
@buffer_limit = buffer_limit
|
403
|
-
@buffer_string = ''.b
|
404
|
-
end
|
405
|
-
|
406
|
-
def write_and_flush
|
407
|
-
write_bytes = @output.write(@buffer_string)
|
408
|
-
while (write_bytes < @buffer_string.bytesize)
|
409
|
-
remaining_byte_range = write_bytes..-1
|
410
|
-
write_bytes += @output.write(@buffer_string.byteslice(remaining_byte_range))
|
411
|
-
end
|
412
|
-
@buffer_string.clear
|
413
|
-
@output.flush
|
414
|
-
write_bytes
|
415
|
-
end
|
416
|
-
private :write_and_flush
|
417
|
-
|
418
|
-
def write(string)
|
419
|
-
@buffer_string << string.b
|
420
|
-
write_and_flush if (@buffer_string.bytesize >= @buffer_limit)
|
421
|
-
end
|
422
|
-
|
423
|
-
def flush
|
424
|
-
write_and_flush unless @buffer_string.empty?
|
425
|
-
self
|
426
|
-
end
|
427
|
-
|
428
|
-
def <<(string)
|
429
|
-
write(string)
|
430
|
-
self
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
434
|
-
class Server
|
435
|
-
DEFAULT = {
|
436
|
-
key_value_store: GDBM_KeyValueStore,
|
437
|
-
use_key_value_store_checksum: true,
|
438
|
-
imap_host: '0.0.0.0'.freeze,
|
439
|
-
imap_port: 1430,
|
440
|
-
send_buffer_limit: 1024 * 16,
|
441
|
-
mail_delivery_user: '#postman'.freeze,
|
442
|
-
process_privilege_uid: 65534,
|
443
|
-
process_privilege_gid: 65534,
|
444
|
-
log_file: 'imap.log'.freeze,
|
445
|
-
log_level: 'INFO',
|
446
|
-
log_stdout: 'INFO',
|
447
|
-
read_lock_timeout_seconds: 30,
|
448
|
-
write_lock_timeout_seconds: 30,
|
449
|
-
cleanup_write_lock_timeout_seconds: 1
|
450
|
-
}.freeze
|
451
|
-
|
452
|
-
def initialize(kvs_meta_open: nil,
|
453
|
-
kvs_text_open: nil,
|
454
|
-
authentication: nil,
|
455
|
-
imap_host: DEFAULT[:imap_host],
|
456
|
-
imap_port: DEFAULT[:imap_port],
|
457
|
-
send_buffer_limit: DEFAULT[:send_buffer_limit],
|
458
|
-
mail_delivery_user: DEFAULT[:mail_delivery_user],
|
459
|
-
process_privilege_uid: DEFAULT[:process_privilege_uid],
|
460
|
-
process_privilege_gid: DEFAULT[:process_privilege_gid],
|
461
|
-
read_lock_timeout_seconds: DEFAULT[:read_lock_timeout_seconds],
|
462
|
-
write_lock_timeout_seconds: DEFAULT[:write_lock_timeout_seconds],
|
463
|
-
cleanup_write_lock_timeout_seconds: DEFAULT[:cleanup_write_lock_timeout_seconds],
|
464
|
-
logger: Logger.new(STDOUT))
|
465
|
-
begin
|
466
|
-
kvs_meta_open or raise ArgumentError, 'need for a keyword argument: kvs_meta_open'
|
467
|
-
kvs_text_open or raise ArgumentError, 'need for a keyword argument: kvs_text_open'
|
468
|
-
@authentication = authentication or raise ArgumentError, 'need for a keyword argument: authentication'
|
469
|
-
|
470
|
-
@imap_host = imap_host
|
471
|
-
@imap_port = imap_port
|
472
|
-
@send_buffer_limit = send_buffer_limit
|
473
|
-
@mail_delivery_user = mail_delivery_user
|
474
|
-
|
475
|
-
@process_privilege_uid = process_privilege_uid
|
476
|
-
@process_privilege_gid = process_privilege_gid
|
477
|
-
|
478
|
-
@read_lock_timeout_seconds = read_lock_timeout_seconds
|
479
|
-
@write_lock_timeout_seconds = write_lock_timeout_seconds
|
480
|
-
@cleanup_write_lock_timeout_seconds = cleanup_write_lock_timeout_seconds
|
481
|
-
|
482
|
-
@logger = logger
|
483
|
-
@mail_store_pool = MailStore.build_pool(kvs_meta_open, kvs_text_open)
|
484
|
-
rescue
|
485
|
-
logger.fatal($!) rescue StandardError
|
486
|
-
raise
|
487
|
-
end
|
488
|
-
end
|
489
|
-
|
490
|
-
def ipaddr_log(addr_list)
|
491
|
-
addr_list.map{|i| "[#{i}]" }.join('')
|
492
|
-
end
|
493
|
-
private :ipaddr_log
|
494
|
-
|
495
|
-
def start
|
496
|
-
@logger.info('start server.')
|
497
|
-
@authentication.start_plug_in(@logger)
|
498
|
-
@logger.info("open socket: #{@imap_host}:#{@imap_port}")
|
499
|
-
sv_sock = TCPServer.new(@imap_host, @imap_port)
|
500
|
-
|
501
|
-
begin
|
502
|
-
@logger.info("opened: #{ipaddr_log(sv_sock.addr)}")
|
503
|
-
|
504
|
-
if (Process.euid == 0) then
|
505
|
-
Process::Sys.setgid(@process_privilege_gid)
|
506
|
-
Process::Sys.setuid(@process_privilege_uid)
|
507
|
-
end
|
508
|
-
|
509
|
-
@logger.info("process ID: #{$$}")
|
510
|
-
process_user = Etc.getpwuid(Process.euid).name rescue ''
|
511
|
-
@logger.info("process privilege user: #{process_user}(#{Process.euid})")
|
512
|
-
process_group = Etc.getgrgid(Process.egid).name rescue ''
|
513
|
-
@logger.info("process privilege group: #{process_group}(#{Process.egid})")
|
514
|
-
|
515
|
-
loop do
|
516
|
-
Thread.start(sv_sock.accept) {|cl_sock|
|
517
|
-
begin
|
518
|
-
@logger.info("accept client: #{ipaddr_log(cl_sock.peeraddr(false))}")
|
519
|
-
decoder = Protocol::Decoder.new_decoder(@mail_store_pool, @authentication, @logger,
|
520
|
-
mail_delivery_user: @mail_delivery_user,
|
521
|
-
read_lock_timeout_seconds: @read_lock_timeout_seconds,
|
522
|
-
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
523
|
-
cleanup_write_lock_timeout_seconds: @cleanup_write_lock_timeout_seconds)
|
524
|
-
Protocol::Decoder.repl(decoder, cl_sock, BufferedWriter.new(cl_sock, @send_buffer_limit), @logger)
|
525
|
-
ensure
|
526
|
-
Error.suppress_2nd_error_at_resource_closing(logger: @logger) { cl_sock.close }
|
527
|
-
end
|
528
|
-
}
|
529
|
-
end
|
530
|
-
ensure
|
531
|
-
@logger.info("close socket: #{ipaddr_log(sv_sock.addr)}")
|
532
|
-
sv_sock.close
|
533
|
-
@authentication.stop_plug_in(@logger)
|
534
|
-
end
|
535
|
-
|
536
|
-
self
|
537
|
-
rescue
|
538
|
-
@logger.error($!)
|
539
|
-
raise
|
540
|
-
ensure
|
541
|
-
@logger.info('stop sever.')
|
542
|
-
end
|
543
|
-
end
|
544
|
-
end
|
545
|
-
|
546
|
-
if ($0 == __FILE__) then
|
547
|
-
require 'pp' if $DEBUG
|
548
|
-
require 'rims'
|
549
|
-
|
550
|
-
if (ARGV.length != 1) then
|
551
|
-
STDERR.puts "usage: #{$0} config.yml"
|
552
|
-
exit(1)
|
553
|
-
end
|
554
|
-
|
555
|
-
c = RIMS::Config.new
|
556
|
-
c.load_config_yaml(ARGV[0])
|
557
|
-
c.setup
|
558
|
-
pp c.config if $DEBUG
|
559
|
-
|
560
|
-
server = RIMS::Server.new(**c.config)
|
561
|
-
server.start
|
562
|
-
end
|
563
|
-
|
564
|
-
# Local Variables:
|
565
|
-
# mode: Ruby
|
566
|
-
# indent-tabs-mode: nil
|
567
|
-
# End:
|