kstor 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/bin/kstor +48 -40
- data/bin/kstor-srv +2 -2
- data/lib/kstor/config.rb +2 -1
- data/lib/kstor/controller/authentication.rb +25 -4
- data/lib/kstor/controller/base.rb +61 -0
- data/lib/kstor/controller/request_handler.rb +84 -16
- data/lib/kstor/controller/secret.rb +63 -64
- data/lib/kstor/controller/users.rb +11 -22
- data/lib/kstor/controller.rb +3 -3
- data/lib/kstor/crypto/armored_value.rb +82 -0
- data/lib/kstor/crypto/keys.rb +9 -41
- data/lib/kstor/crypto.rb +0 -1
- data/lib/kstor/error.rb +36 -3
- data/lib/kstor/log/simple_logger.rb +83 -0
- data/lib/kstor/log/systemd_logger.rb +22 -0
- data/lib/kstor/log.rb +53 -4
- data/lib/kstor/message/base.rb +223 -0
- data/lib/kstor/message/error.rb +15 -0
- data/lib/kstor/message/group_create.rb +14 -0
- data/lib/kstor/message/group_created.rb +16 -0
- data/lib/kstor/message/ping.rb +14 -0
- data/lib/kstor/message/pong.rb +14 -0
- data/lib/kstor/message/secret_create.rb +16 -0
- data/lib/kstor/message/secret_created.rb +14 -0
- data/lib/kstor/message/secret_delete.rb +14 -0
- data/lib/kstor/message/secret_deleted.rb +14 -0
- data/lib/kstor/message/secret_list.rb +14 -0
- data/lib/kstor/message/secret_search.rb +14 -0
- data/lib/kstor/message/secret_unlock.rb +14 -0
- data/lib/kstor/message/secret_update_meta.rb +15 -0
- data/lib/kstor/message/secret_update_value.rb +15 -0
- data/lib/kstor/message/secret_updated.rb +14 -0
- data/lib/kstor/message/secret_value.rb +35 -0
- data/lib/kstor/message.rb +26 -113
- data/lib/kstor/model.rb +42 -25
- data/lib/kstor/server.rb +15 -3
- data/lib/kstor/session.rb +34 -2
- data/lib/kstor/socket_server.rb +20 -1
- data/lib/kstor/sql_connection.rb +16 -1
- data/lib/kstor/store.rb +182 -72
- data/lib/kstor/systemd.rb +5 -0
- data/lib/kstor/version.rb +2 -1
- data/lib/kstor.rb +1 -1
- metadata +25 -3
data/lib/kstor/message.rb
CHANGED
@@ -3,130 +3,43 @@
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
module KStor
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# We can't use a KStor::Error here because kstor/error.rb require()s
|
10
|
-
# kstor/message.rb .
|
11
|
-
class RequestMissesAuthData < RuntimeError
|
12
|
-
end
|
13
|
-
|
14
|
-
class UnparsableResponse < RuntimeError
|
15
|
-
end
|
16
|
-
|
17
|
-
# A user request or response.
|
18
|
-
class Message
|
19
|
-
attr_reader :type
|
20
|
-
attr_reader :args
|
21
|
-
|
22
|
-
def initialize(type, args)
|
23
|
-
@type = type
|
24
|
-
@args = args
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
{ 'type' => @type, 'args' => @args }
|
29
|
-
end
|
30
|
-
|
31
|
-
def serialize
|
32
|
-
to_h.to_json
|
33
|
-
end
|
34
|
-
|
35
|
-
# Parse a user request, freshly arrived from the network.
|
6
|
+
module Message
|
7
|
+
# Internal exception when a request is received with neither a session ID
|
8
|
+
# nor a login/password pair.
|
36
9
|
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
40
|
-
def self.parse_request(str)
|
41
|
-
data = JSON.parse(str)
|
42
|
-
if data.key?('login') && data.key?('password')
|
43
|
-
LoginRequest.new(
|
44
|
-
data['login'], data['password'],
|
45
|
-
data['type'], data['args']
|
46
|
-
)
|
47
|
-
elsif data.key?('session_id')
|
48
|
-
SessionRequest.new(
|
49
|
-
data['session_id'], data['type'], data['args']
|
50
|
-
)
|
51
|
-
else
|
52
|
-
raise RequestMissesAuthData
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# A user authentication request.
|
58
|
-
class LoginRequest < Message
|
59
|
-
attr_reader :login
|
60
|
-
attr_reader :password
|
61
|
-
|
62
|
-
def initialize(login, password, type, args)
|
63
|
-
@login = login
|
64
|
-
@password = password
|
65
|
-
super(type, args)
|
66
|
-
end
|
67
|
-
|
68
|
-
def inspect
|
69
|
-
fmt = [
|
70
|
-
'#<KStor::LoginRequest:%<id>x',
|
71
|
-
'@login=%<login>s',
|
72
|
-
'@password="******"',
|
73
|
-
'@args=%<args>s>'
|
74
|
-
].join(' ')
|
75
|
-
format(fmt, id: object_id, login: @login.inspect, args: @args.inspect)
|
10
|
+
# We can't use a KStor::Error here because kstor/error.rb require()s
|
11
|
+
# kstor/message.rb .
|
12
|
+
class RequestMissesAuthData < RuntimeError
|
76
13
|
end
|
77
14
|
|
78
|
-
|
79
|
-
|
15
|
+
# Response data was invalid JSON.
|
16
|
+
class UnparsableResponse < RuntimeError
|
80
17
|
end
|
81
18
|
end
|
19
|
+
end
|
82
20
|
|
83
|
-
|
84
|
-
class SessionRequest < Message
|
85
|
-
attr_reader :session_id
|
21
|
+
require 'kstor/message/error'
|
86
22
|
|
87
|
-
|
88
|
-
|
89
|
-
super(type, args)
|
90
|
-
end
|
23
|
+
require 'kstor/message/ping'
|
24
|
+
require 'kstor/message/pong'
|
91
25
|
|
92
|
-
|
93
|
-
|
94
|
-
'#<KStor::SessionRequest:%<id>x',
|
95
|
-
'@session_id=******',
|
96
|
-
'@args=%<args>s>'
|
97
|
-
].join(' ')
|
98
|
-
format(fmt, id: object_id, args: @args.inspect)
|
99
|
-
end
|
26
|
+
require 'kstor/message/group_create'
|
27
|
+
require 'kstor/message/group_created'
|
100
28
|
|
101
|
-
|
102
|
-
|
103
|
-
end
|
104
|
-
end
|
29
|
+
require 'kstor/message/secret_create'
|
30
|
+
require 'kstor/message/secret_created'
|
105
31
|
|
106
|
-
|
107
|
-
|
108
|
-
attr_accessor :session_id
|
32
|
+
require 'kstor/message/secret_delete'
|
33
|
+
require 'kstor/message/secret_deleted'
|
109
34
|
|
110
|
-
|
111
|
-
|
112
|
-
super
|
113
|
-
end
|
35
|
+
require 'kstor/message/secret_search'
|
36
|
+
require 'kstor/message/secret_list'
|
114
37
|
|
115
|
-
|
116
|
-
|
117
|
-
resp = new(data['type'], data['args'])
|
118
|
-
resp.session_id = data['session_id']
|
119
|
-
resp
|
120
|
-
rescue JSON::ParserError => e
|
121
|
-
raise UnparsableResponse, e.message
|
122
|
-
end
|
38
|
+
require 'kstor/message/secret_unlock'
|
39
|
+
require 'kstor/message/secret_value'
|
123
40
|
|
124
|
-
|
125
|
-
|
126
|
-
|
41
|
+
require 'kstor/message/secret_update_meta'
|
42
|
+
require 'kstor/message/secret_update_value'
|
43
|
+
require 'kstor/message/secret_updated'
|
127
44
|
|
128
|
-
|
129
|
-
super.merge('session_id' => @session_id)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
45
|
+
KStor::Message.register_all_message_types
|
data/lib/kstor/model.rb
CHANGED
@@ -86,10 +86,7 @@ module KStor
|
|
86
86
|
|
87
87
|
# Dump properties except pubk.
|
88
88
|
def to_h
|
89
|
-
|
90
|
-
h.delete('pubk')
|
91
|
-
|
92
|
-
h
|
89
|
+
super.except('pubk')
|
93
90
|
end
|
94
91
|
end
|
95
92
|
|
@@ -154,9 +151,7 @@ module KStor
|
|
154
151
|
|
155
152
|
# Dump properties except {#encrypted_privk}.
|
156
153
|
def to_h
|
157
|
-
|
158
|
-
h.delete('encrypted_privk')
|
159
|
-
h
|
154
|
+
super.except('encrypted_privk')
|
160
155
|
end
|
161
156
|
end
|
162
157
|
|
@@ -263,9 +258,6 @@ module KStor
|
|
263
258
|
self.kdf_params = secret_key.kdf_params
|
264
259
|
encrypt(secret_key)
|
265
260
|
self.keychain = {}
|
266
|
-
# FIXME: delete keychain items from database!
|
267
|
-
# they won't be useable (decryption key is lost) but will provoke
|
268
|
-
# errors.
|
269
261
|
end
|
270
262
|
|
271
263
|
# Re-encrypt private key and keychain with a new secret key derived from
|
@@ -283,9 +275,7 @@ module KStor
|
|
283
275
|
|
284
276
|
# Dump properties except {#encrypted_privk} and {#pubk}.
|
285
277
|
def to_h
|
286
|
-
h = super
|
287
|
-
h.delete('encrypted_privk')
|
288
|
-
h.delete('pubk')
|
278
|
+
h = super.except('encrypted_privk', 'pubk')
|
289
279
|
h['keychain'] = keychain.transform_values(&:to_h) if keychain
|
290
280
|
|
291
281
|
h
|
@@ -320,6 +310,13 @@ module KStor
|
|
320
310
|
# Secret should be used at this URL
|
321
311
|
attr_accessor :url
|
322
312
|
|
313
|
+
# Create new metadata for a secret.
|
314
|
+
#
|
315
|
+
# Hash param can contains keys for "app", "database", "login", "server"
|
316
|
+
# and "url". Any other key is ignored.
|
317
|
+
#
|
318
|
+
# @param values [Hash, KStor::Crypto::ArmoredHash] metadata
|
319
|
+
# @return [KStor::Model::SecretMeta] secret metadata
|
323
320
|
def initialize(values)
|
324
321
|
@app = values['app']
|
325
322
|
@database = values['database']
|
@@ -328,30 +325,50 @@ module KStor
|
|
328
325
|
@url = values['url']
|
329
326
|
end
|
330
327
|
|
328
|
+
# Convert this metadata to a Hash.
|
329
|
+
#
|
330
|
+
# Empty values will not be included.
|
331
|
+
#
|
332
|
+
# @return [Hash[String, String]] metadata as a Hash
|
331
333
|
def to_h
|
332
334
|
{ 'app' => @app, 'database' => @database, 'login' => @login,
|
333
335
|
'server' => @server, 'url' => @url }.compact
|
334
336
|
end
|
335
337
|
|
338
|
+
# Prepare metadata to be written to disk or database.
|
339
|
+
#
|
340
|
+
# @return [KStor::Crypto::ArmoredHash] serialized metadata
|
336
341
|
def serialize
|
337
342
|
Crypto::ArmoredHash.from_hash(to_h)
|
338
343
|
end
|
339
344
|
|
345
|
+
# Merge metadata.
|
346
|
+
#
|
347
|
+
# @param other [KStor::Model::SecretMeta] other metadata that will
|
348
|
+
# override this object's values.
|
340
349
|
def merge(other)
|
341
350
|
values = to_h.merge(other.to_h)
|
342
351
|
values.reject! { |_, v| v.empty? }
|
343
352
|
self.class.new(values)
|
344
353
|
end
|
345
354
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
355
|
+
# Match against wildcards.
|
356
|
+
#
|
357
|
+
# Metadata will be matched against another metadata object with wildcard
|
358
|
+
# values. This uses roughly the same rules that shell wildcards (e.g.
|
359
|
+
# fnmatch(3) C function).
|
360
|
+
#
|
361
|
+
# @see File.fnmatch?
|
362
|
+
#
|
363
|
+
# @param meta [KStor::Model::SecretMeta] wildcard metadata
|
364
|
+
# @return [Boolean] true if matched
|
365
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
350
366
|
def match?(meta)
|
351
367
|
self_h = to_h
|
352
368
|
other_h = meta.to_h
|
353
369
|
other_h.each do |k, wildcard|
|
354
370
|
val = self_h[k]
|
371
|
+
return false if val.nil? && !wildcard.nil? && wildcard != '*'
|
355
372
|
next if val.nil?
|
356
373
|
next if wildcard.nil?
|
357
374
|
|
@@ -362,6 +379,7 @@ module KStor
|
|
362
379
|
end
|
363
380
|
true
|
364
381
|
end
|
382
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
365
383
|
end
|
366
384
|
|
367
385
|
# A secret, with metadata and a value that are kept encrypted on disk.
|
@@ -383,8 +401,11 @@ module KStor
|
|
383
401
|
# @!macro dsl_model_properties_rw
|
384
402
|
property :metadata, read_only: true
|
385
403
|
|
404
|
+
# Set metadata (or unset if nil).
|
405
|
+
#
|
406
|
+
# @param armored_hash [KStor::Crypt::ArmoredHash] metadata to load
|
386
407
|
def metadata=(armored_hash)
|
387
|
-
@data[:metadata] = armored_hash ? SecretMeta.
|
408
|
+
@data[:metadata] = armored_hash ? SecretMeta.new(armored_hash) : nil
|
388
409
|
end
|
389
410
|
|
390
411
|
# Decrypt secret value.
|
@@ -424,13 +445,9 @@ module KStor
|
|
424
445
|
# Dump properties except {#ciphertext}, {#encrypted_metadata},
|
425
446
|
# {#value_author_id} and {#meta_author_id}.
|
426
447
|
def to_h
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
h.delete('value_author_id')
|
431
|
-
h.delete('meta_author_id')
|
432
|
-
|
433
|
-
h
|
448
|
+
super.except(
|
449
|
+
*%w[ciphertext encrypted_metadata value_author_id meta_author_id]
|
450
|
+
)
|
434
451
|
end
|
435
452
|
end
|
436
453
|
end
|
data/lib/kstor/server.rb
CHANGED
@@ -15,11 +15,24 @@ module KStor
|
|
15
15
|
|
16
16
|
# Listen for clients and respond to their requests.
|
17
17
|
class Server < SocketServer
|
18
|
+
# Create a new server object.
|
19
|
+
#
|
20
|
+
# @param controller [KStor::Controller::RequestHandler] client request
|
21
|
+
# handler
|
22
|
+
# @param args [Hash] parameters to apss to parent class
|
23
|
+
# {KStor::SocketServer}
|
24
|
+
# @return [KStor::Server] new KStor server.
|
18
25
|
def initialize(controller:, **args)
|
19
26
|
@controller = controller
|
20
27
|
super(**args)
|
21
28
|
end
|
22
29
|
|
30
|
+
# Implement {KStor::SocketServer#work} to actually serve clients.
|
31
|
+
#
|
32
|
+
# This method must read client request, write a response and close the
|
33
|
+
# socket.
|
34
|
+
#
|
35
|
+
# @param client [Socket] channel of communication to a client
|
23
36
|
def work(client)
|
24
37
|
client_data, = client.recvfrom(4096)
|
25
38
|
Log.debug("server: read #{client_data.bytesize} bytes from client")
|
@@ -36,15 +49,14 @@ module KStor
|
|
36
49
|
private
|
37
50
|
|
38
51
|
def handle_client_data(data)
|
39
|
-
req = Message.
|
52
|
+
req = Message::Base.parse(data)
|
40
53
|
resp = @controller.handle_request(req)
|
41
|
-
Log.debug("server: response = #{resp.inspect}")
|
42
54
|
resp.serialize
|
43
55
|
rescue JSON::ParserError => e
|
44
56
|
err = Error.for_code('MSG/INVALID', e.message)
|
45
57
|
Log.info("server: #{err}")
|
46
58
|
err.response.serialize
|
47
|
-
rescue RequestMissesAuthData
|
59
|
+
rescue Message::RequestMissesAuthData
|
48
60
|
raise Error.for_code('MSG/INVALID', 'Missing authentication data')
|
49
61
|
end
|
50
62
|
end
|
data/lib/kstor/session.rb
CHANGED
@@ -11,6 +11,13 @@ module KStor
|
|
11
11
|
attr_reader :created_at
|
12
12
|
attr_reader :updated_at
|
13
13
|
|
14
|
+
# Create a new user session.
|
15
|
+
#
|
16
|
+
# @param sid [String] Session ID
|
17
|
+
# @param user [KStor::Model::User] user owning the session
|
18
|
+
# @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
|
19
|
+
# from password
|
20
|
+
# @return [KStor::Session] new session
|
14
21
|
def initialize(sid, user, secret_key)
|
15
22
|
@id = sid
|
16
23
|
@user = user
|
@@ -19,21 +26,37 @@ module KStor
|
|
19
26
|
@updated_at = Time.now
|
20
27
|
end
|
21
28
|
|
29
|
+
# Update access time, for idle sessions weeding.
|
30
|
+
#
|
31
|
+
# @return [KStor::Session] updated session
|
22
32
|
def update
|
23
33
|
@updated_at = Time.now
|
24
34
|
self
|
25
35
|
end
|
26
36
|
|
37
|
+
# Create a new session for a user.
|
38
|
+
#
|
39
|
+
# @param user [KStor::Model::User] user owning the session
|
40
|
+
# @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
|
41
|
+
# from password
|
42
|
+
# @return [KStor::Session] new session with a random SID
|
27
43
|
def self.create(user, secret_key)
|
28
44
|
sid = SecureRandom.urlsafe_base64(16)
|
29
45
|
new(sid, user, secret_key)
|
30
46
|
end
|
31
47
|
end
|
32
48
|
|
33
|
-
# Collection of user sessions (in memory)
|
49
|
+
# Collection of user sessions (in memory).
|
34
50
|
#
|
35
|
-
#
|
51
|
+
# Concurrent accesses are synchronized on a mutex.
|
36
52
|
class SessionStore
|
53
|
+
# Create new session store.
|
54
|
+
#
|
55
|
+
# @param idle_timeout [Integer] sessions that aren't updated for this
|
56
|
+
# number of seconds are considered invalid
|
57
|
+
# @param life_timeout [Integer] sessions that are older than this number of
|
58
|
+
# seconds are considered invalid
|
59
|
+
# @return [KStor::SessionStore] a new session store.
|
37
60
|
def initialize(idle_timeout, life_timeout)
|
38
61
|
@idle_timeout = idle_timeout
|
39
62
|
@life_timeout = life_timeout
|
@@ -41,12 +64,20 @@ module KStor
|
|
41
64
|
@sessions.extend(Mutex_m)
|
42
65
|
end
|
43
66
|
|
67
|
+
# Add a session to the store.
|
68
|
+
#
|
69
|
+
# @param session [KStor::Session] session to store
|
44
70
|
def <<(session)
|
45
71
|
@sessions.synchronize do
|
46
72
|
@sessions[session.id] = session
|
47
73
|
end
|
48
74
|
end
|
49
75
|
|
76
|
+
# Fetch a session from it's ID.
|
77
|
+
#
|
78
|
+
# @param sid [String] session ID to lookup
|
79
|
+
# @return [KStor::Session, nil] session or nil if session ID was not found
|
80
|
+
# or session has expired
|
50
81
|
def [](sid)
|
51
82
|
@sessions.synchronize do
|
52
83
|
s = @sessions[sid.to_s]
|
@@ -61,6 +92,7 @@ module KStor
|
|
61
92
|
end
|
62
93
|
end
|
63
94
|
|
95
|
+
# Delete expired sessions.
|
64
96
|
def purge
|
65
97
|
now = Time.now
|
66
98
|
@sessions.synchronize do
|
data/lib/kstor/socket_server.rb
CHANGED
@@ -9,8 +9,14 @@ require 'timeout'
|
|
9
9
|
module KStor
|
10
10
|
# Serve clients on UNIX sockets.
|
11
11
|
class SocketServer
|
12
|
+
# Wait this number of seconds for worker threads to terminate before
|
13
|
+
# killing them without mercy.
|
12
14
|
GRACEFUL_TIMEOUT = 10
|
13
15
|
|
16
|
+
# Create a new server.
|
17
|
+
#
|
18
|
+
# @param socket_path [String] path to listening socket
|
19
|
+
# @param nworkers [Integer] number of worker threads
|
14
20
|
def initialize(socket_path:, nworkers:)
|
15
21
|
@path = socket_path
|
16
22
|
@nworkers = nworkers
|
@@ -18,10 +24,14 @@ module KStor
|
|
18
24
|
@workers = []
|
19
25
|
end
|
20
26
|
|
27
|
+
# Start serving clients.
|
28
|
+
#
|
29
|
+
# Send interrupt signal to cleanly stop.
|
21
30
|
def start
|
22
31
|
start_workers
|
23
|
-
server =
|
32
|
+
server = server_socket
|
24
33
|
Systemd.service_ready
|
34
|
+
Log.info('socket_server: started')
|
25
35
|
loop do
|
26
36
|
maintain_workers
|
27
37
|
@client_queue.enq(server.accept.first)
|
@@ -31,6 +41,8 @@ module KStor
|
|
31
41
|
Log.info('socket_server: stopped.')
|
32
42
|
end
|
33
43
|
|
44
|
+
# Do some work for a client and write a response.
|
45
|
+
# @abstract
|
34
46
|
def work(client)
|
35
47
|
# Abstract method.
|
36
48
|
client.close
|
@@ -38,6 +50,13 @@ module KStor
|
|
38
50
|
|
39
51
|
private
|
40
52
|
|
53
|
+
def server_socket
|
54
|
+
s = Systemd.socket
|
55
|
+
return s if s
|
56
|
+
|
57
|
+
UNIXServer.new(@path)
|
58
|
+
end
|
59
|
+
|
41
60
|
def worker_run
|
42
61
|
while (client = @client_queue.deq)
|
43
62
|
Log.debug("socket_server: #{Thread.current.name} serving one client")
|
data/lib/kstor/sql_connection.rb
CHANGED
@@ -14,18 +14,33 @@ module KStor
|
|
14
14
|
|
15
15
|
# Execute SQL commands in a per-thread SQLite connection.
|
16
16
|
class SQLConnection
|
17
|
+
# Create a new SQL connection.
|
18
|
+
#
|
19
|
+
# @param file_path [String] path to SQLite database
|
20
|
+
# @return [KStor::SQLConnection] the new connection
|
17
21
|
def initialize(file_path)
|
18
22
|
@file_path = file_path
|
19
23
|
end
|
20
24
|
|
25
|
+
# Execute SQL statement.
|
26
|
+
#
|
27
|
+
# @param sql [String] SQL statement
|
28
|
+
# @param params [Array] parameters to fill placeholders in the statement
|
29
|
+
# @return [Any] Whatever SQLite returns
|
21
30
|
def execute(sql, *params, &)
|
22
31
|
database.execute(sql, *params, &)
|
23
32
|
end
|
24
33
|
|
34
|
+
# Last generated ID (from an INSERT statement).
|
35
|
+
#
|
36
|
+
# @return [Integer] generated ID from last insert statement.
|
25
37
|
def last_insert_row_id
|
26
38
|
database.last_insert_row_id
|
27
39
|
end
|
28
40
|
|
41
|
+
# Execute given block of code in a database transaction.
|
42
|
+
#
|
43
|
+
# @return [Any] Whatever the block returns
|
29
44
|
def transaction(&block)
|
30
45
|
result = nil
|
31
46
|
database.transaction do |db|
|
@@ -52,7 +67,7 @@ module KStor
|
|
52
67
|
def setup_thread_connection(key)
|
53
68
|
return if Thread.current[key]
|
54
69
|
|
55
|
-
Log.
|
70
|
+
Log.debug(
|
56
71
|
"sqlite: initializing connection in thread #{Thread.current.name}"
|
57
72
|
)
|
58
73
|
Thread.current[key] = connect(@file_path)
|