Make MozillaStore#close actually "work" by closing open statements.

Add a finalizer to MozillaStore also, which automatically closes the
SQLite3 database.
This commit is contained in:
Akinori MUSHA 2013-04-16 00:04:54 +09:00
parent db58d2c8ab
commit ded02f8327
2 changed files with 194 additions and 115 deletions

View file

@ -9,6 +9,7 @@ class HTTP::CookieJar
# Session cookies are stored separately on memory and will not be # Session cookies are stored separately on memory and will not be
# stored persistently in the SQLite3 database. # stored persistently in the SQLite3 database.
class MozillaStore < AbstractStore class MozillaStore < AbstractStore
# :stopdoc:
SCHEMA_VERSION = 5 SCHEMA_VERSION = 5
def default_options def default_options
@ -32,6 +33,41 @@ class HTTP::CookieJar
appId inBrowserElement appId inBrowserElement
] ]
SQL = {}
Callable = proc { |obj, meth, *args|
proc {
obj.__send__(meth, *args)
}
}
class Database < SQLite3::Database
def initialize(file, options = {})
@stmts = []
options = {
:results_as_hash => true,
}.update(options)
super
end
def prepare(sql)
case st = super
when SQLite3::Statement
@stmts << st
end
st
end
def close
return self if closed?
@stmts.reject! { |st|
st.closed? || st.close
}
super
end
end
# :startdoc:
# Generates a Mozilla cookie store. If the file does not exist, # Generates a Mozilla cookie store. If the file does not exist,
# it is created. If it does and its schema is old, it is # it is created. If it does and its schema is old, it is
# automatically upgraded with a new schema keeping the existing # automatically upgraded with a new schema keeping the existing
@ -59,8 +95,14 @@ class HTTP::CookieJar
@filename = options[:filename] or raise ArgumentError, ':filename option is missing' @filename = options[:filename] or raise ArgumentError, ':filename option is missing'
@sjar = HashStore.new @sjar = HashStore.new
@db = SQLite3::Database.new(@filename)
@db.results_as_hash = true @db = Database.new(@filename)
@stmt = Hash.new { |st, key|
st[key] = @db.prepare(SQL[key])
}
ObjectSpace.define_finalizer(self, Callable[@db, :close])
upgrade_database upgrade_database
@ -126,6 +168,13 @@ class HTTP::CookieJar
SQL SQL
end end
def db_prepare(sql)
st = @db.prepare(sql)
yield st
ensure
st.close if st
end
def upgrade_database def upgrade_database
loop { loop {
case schema_version case schema_version
@ -138,19 +187,18 @@ class HTTP::CookieJar
when 2 when 2
@db.execute("ALTER TABLE moz_cookies ADD baseDomain TEXT") @db.execute("ALTER TABLE moz_cookies ADD baseDomain TEXT")
st_update = @db.prepare("UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id") db_prepare("UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id") { |st_update|
@db.execute("SELECT id, host FROM moz_cookies") { |row| @db.execute("SELECT id, host FROM moz_cookies") { |row|
domain_name = DomainName.new(row['host'][/\A\.?(.*)/, 1]) domain_name = DomainName.new(row['host'][/\A\.?(.*)/, 1])
domain = domain_name.domain || domain_name.hostname domain = domain_name.domain || domain_name.hostname
st_update.execute(:baseDomain => domain, :id => row['id']) st_update.execute(:baseDomain => domain, :id => row['id'])
} }
}
@db.execute("CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)") @db.execute("CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")
self.schema_version += 1 self.schema_version += 1
when 3 when 3
st_delete = @db.prepare("DELETE FROM moz_cookies WHERE id = :id") db_prepare("DELETE FROM moz_cookies WHERE id = :id") { |st_delete|
prev_row = nil prev_row = nil
@db.execute(<<-'SQL') { |row| @db.execute(<<-'SQL') { |row|
SELECT id, name, host, path FROM moz_cookies SELECT id, name, host, path FROM moz_cookies
@ -161,6 +209,7 @@ class HTTP::CookieJar
end end
prev_row = row prev_row = row
} }
}
@db.execute("ALTER TABLE moz_cookies ADD creationTime INTEGER") @db.execute("ALTER TABLE moz_cookies ADD creationTime INTEGER")
@db.execute("UPDATE moz_cookies SET creationTime = (SELECT id WHERE id = moz_cookies.id)") @db.execute("UPDATE moz_cookies SET creationTime = (SELECT id WHERE id = moz_cookies.id)")
@ -192,14 +241,15 @@ class HTTP::CookieJar
end end
end end
def db_add(cookie) SQL[:add] = <<-'SQL' % [
@st_add ||= INSERT OR REPLACE INTO moz_cookies (%s) VALUES (%s)
@db.prepare('INSERT OR REPLACE INTO moz_cookies (%s) VALUES (%s)' % [ SQL
ALL_COLUMNS.join(', '), ALL_COLUMNS.join(', '),
ALL_COLUMNS.map { |col| ":#{col}" }.join(', ') ALL_COLUMNS.map { |col| ":#{col}" }.join(', ')
]) ]
@st_add.execute({ def db_add(cookie)
@stmt[:add].execute({
:baseDomain => cookie.domain_name.domain || cookie.domain, :baseDomain => cookie.domain_name.domain || cookie.domain,
:appId => @app_id, :appId => @app_id,
:inBrowserElement => @in_browser_element ? 1 : 0, :inBrowserElement => @in_browser_element ? 1 : 0,
@ -217,9 +267,7 @@ class HTTP::CookieJar
self self
end end
def db_delete(cookie) SQL[:delete] = <<-'SQL'
@st_delete ||=
@db.prepare(<<-'SQL')
DELETE FROM moz_cookies DELETE FROM moz_cookies
WHERE appId = :appId AND WHERE appId = :appId AND
inBrowserElement = :inBrowserElement AND inBrowserElement = :inBrowserElement AND
@ -228,7 +276,8 @@ class HTTP::CookieJar
path = :path path = :path
SQL SQL
@st_delete.execute({ def db_delete(cookie)
@stmt[:delete].execute({
:appId => @app_id, :appId => @app_id,
:inBrowserElement => @in_browser_element ? 1 : 0, :inBrowserElement => @in_browser_element ? 1 : 0,
:name => cookie.name, :name => cookie.name,
@ -255,11 +304,7 @@ class HTTP::CookieJar
db_delete(cookie) db_delete(cookie)
end end
def each(uri = nil, &block) SQL[:cookies_for_domain] = <<-'SQL'
now = Time.now
if uri
@st_cookies_for_domain ||=
@db.prepare(<<-'SQL')
SELECT * FROM moz_cookies SELECT * FROM moz_cookies
WHERE baseDomain = :baseDomain AND WHERE baseDomain = :baseDomain AND
appId = :appId AND appId = :appId AND
@ -267,13 +312,26 @@ class HTTP::CookieJar
expiry >= :expiry expiry >= :expiry
SQL SQL
@st_update_lastaccessed ||= SQL[:update_lastaccessed] = <<-'SQL'
@db.prepare("UPDATE moz_cookies SET lastAccessed = :lastAccessed where id = :id") UPDATE moz_cookies
SET lastAccessed = :lastAccessed
WHERE id = :id
SQL
SQL[:all_cookies] = <<-'SQL'
SELECT * FROM moz_cookies
WHERE appId = :appId AND
inBrowserElement = :inBrowserElement AND
expiry >= :expiry
SQL
def each(uri = nil, &block)
now = Time.now
if uri
thost = DomainName.new(uri.host) thost = DomainName.new(uri.host)
tpath = uri.path tpath = uri.path
@st_cookies_for_domain.execute({ @stmt[:cookies_for_domain].execute({
:baseDomain => thost.domain || thost.hostname, :baseDomain => thost.domain || thost.hostname,
:appId => @app_id, :appId => @app_id,
:inBrowserElement => @in_browser_element ? 1 : 0, :inBrowserElement => @in_browser_element ? 1 : 0,
@ -297,7 +355,7 @@ class HTTP::CookieJar
if cookie.valid_for_uri?(uri) if cookie.valid_for_uri?(uri)
cookie.accessed_at = now cookie.accessed_at = now
@st_update_lastaccessed.execute({ @stmt[:update_lastaccessed].execute({
'lastAccessed' => now.to_i, 'lastAccessed' => now.to_i,
'id' => row['id'], 'id' => row['id'],
}) })
@ -306,15 +364,7 @@ class HTTP::CookieJar
} }
@sjar.each(uri, &block) @sjar.each(uri, &block)
else else
@st_all_cookies ||= @stmt[:all_cookies].execute({
@db.prepare(<<-'SQL')
SELECT * FROM moz_cookies
WHERE appId = :appId AND
inBrowserElement = :inBrowserElement AND
expiry >= :expiry
SQL
@st_all_cookies.execute({
:appId => @app_id, :appId => @app_id,
:inBrowserElement => @in_browser_element ? 1 : 0, :inBrowserElement => @in_browser_element ? 1 : 0,
:expiry => now.to_i, :expiry => now.to_i,
@ -344,11 +394,12 @@ class HTTP::CookieJar
self self
end end
def count SQL[:count] = <<-'SQL'
@st_count ||= SELECT COUNT(id) FROM moz_cookies
@db.prepare("SELECT COUNT(id) FROM moz_cookies") SQL
@st_count.execute.first[0] def count
@stmt[:count].execute.first[0]
end end
protected :count protected :count
@ -356,44 +407,43 @@ class HTTP::CookieJar
@sjar.empty? && count == 0 @sjar.empty? && count == 0
end end
def cleanup(session = false) SQL[:delete_expired] = <<-'SQL'
@st_delete_expired ||= DELETE FROM moz_cookies WHERE expiry < :expiry
@db.prepare("DELETE FROM moz_cookies WHERE expiry < :expiry") SQL
@st_overusing_domains ||= SQL[:overusing_domains] = <<-'SQL'
@db.prepare(<<-'SQL')
SELECT LTRIM(host, '.') domain, COUNT(*) count SELECT LTRIM(host, '.') domain, COUNT(*) count
FROM moz_cookies FROM moz_cookies
GROUP BY domain GROUP BY domain
HAVING count > :count HAVING count > :count
SQL SQL
@st_delete_per_domain_overuse ||= SQL[:delete_per_domain_overuse] = <<-'SQL'
@db.prepare(<<-'SQL')
DELETE FROM moz_cookies WHERE id IN ( DELETE FROM moz_cookies WHERE id IN (
SELECT id FROM moz_cookies SELECT id FROM moz_cookies
WHERE LTRIM(host, '.') = :domain WHERE LTRIM(host, '.') = :domain
ORDER BY creationtime ORDER BY creationtime
LIMIT :limit) LIMIT :limit)
SQL SQL
@st_delete_total_overuse ||=
@db.prepare(<<-'SQL') SQL[:delete_total_overuse] = <<-'SQL'
DELETE FROM moz_cookies WHERE id IN ( DELETE FROM moz_cookies WHERE id IN (
SELECT id FROM moz_cookies ORDER BY creationTime ASC LIMIT :limit SELECT id FROM moz_cookies ORDER BY creationTime ASC LIMIT :limit
) )
SQL SQL
def cleanup(session = false)
synchronize { synchronize {
break if @gc_index == 0 break if @gc_index == 0
@st_delete_expired.execute({ 'expiry' => Time.now.to_i }) @stmt[:delete_expired].execute({ 'expiry' => Time.now.to_i })
@st_overusing_domains.execute({ @stmt[:overusing_domains].execute({
'count' => HTTP::Cookie::MAX_COOKIES_PER_DOMAIN 'count' => HTTP::Cookie::MAX_COOKIES_PER_DOMAIN
}).each { |row| }).each { |row|
domain, count = row['domain'], row['count'] domain, count = row['domain'], row['count']
@st_delete_per_domain_overuse.execute({ @stmt[:delete_per_domain_overuse].execute({
'domain' => domain, 'domain' => domain,
'limit' => count - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN, 'limit' => count - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN,
}) })
@ -402,7 +452,7 @@ class HTTP::CookieJar
overrun = count - HTTP::Cookie::MAX_COOKIES_TOTAL overrun = count - HTTP::Cookie::MAX_COOKIES_TOTAL
if overrun > 0 if overrun > 0
@st_delete_total_overuse.execute({ 'limit' => overrun }) @stmt[:delete_total_overuse].execute({ 'limit' => overrun })
end end
@gc_index = 0 @gc_index = 0

View file

@ -2,25 +2,7 @@ require File.expand_path('helper', File.dirname(__FILE__))
require 'tmpdir' require 'tmpdir'
module TestHTTPCookieJar module TestHTTPCookieJar
class TestBasic < Test::Unit::TestCase module CommonTests
def test_store
jar = HTTP::CookieJar.new(:store => :hash)
assert_instance_of HTTP::CookieJar::HashStore, jar.store
assert_raises(IndexError) {
jar = HTTP::CookieJar.new(:store => :nonexistent)
}
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore.new)
assert_instance_of HTTP::CookieJar::HashStore, jar.store
assert_raises(TypeError) {
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore)
}
end
end
module Tests
def setup(options = nil, options2 = nil) def setup(options = nil, options2 = nil)
default_options = { default_options = {
:store => :hash, :store => :hash,
@ -770,11 +752,28 @@ module TestHTTPCookieJar
end end
class WithHashStore < Test::Unit::TestCase class WithHashStore < Test::Unit::TestCase
include Tests include CommonTests
def test_new
jar = HTTP::CookieJar.new(:store => :hash)
assert_instance_of HTTP::CookieJar::HashStore, jar.store
assert_raises(IndexError) {
jar = HTTP::CookieJar.new(:store => :nonexistent)
}
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore.new)
assert_instance_of HTTP::CookieJar::HashStore, jar.store
assert_raises(TypeError) {
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore)
}
end
end end
class WithMozillaStore < Test::Unit::TestCase class WithMozillaStore < Test::Unit::TestCase
include Tests include CommonTests
def setup def setup
super( super(
@ -782,6 +781,36 @@ module TestHTTPCookieJar
{ :store => :mozilla, :filename => ":memory:" }) { :store => :mozilla, :filename => ":memory:" })
end end
def add_and_delete(jar)
jar.parse("name=Akinori; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/",
'http://rubyforge.org/')
jar.parse("country=Japan; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/",
'http://rubyforge.org/')
jar.delete(HTTP::Cookie.new("name", :domain => 'rubyforge.org'))
end
def test_close
add_and_delete(@jar)
assert_not_send [@jar.store, :closed?]
@jar.store.close
assert_send [@jar.store, :closed?]
@jar.store.close # should do nothing
assert_send [@jar.store, :closed?]
end
def test_finalizer
db = nil
loop {
jar = HTTP::CookieJar.new(:store => :mozilla, :filename => ':memory:')
add_and_delete(jar)
db = jar.store.instance_variable_get(:@db)
break
}
GC.start
assert_send [db, :closed?]
end
def test_upgrade_mozillastore def test_upgrade_mozillastore
Dir.mktmpdir { |dir| Dir.mktmpdir { |dir|
filename = File.join(dir, 'cookies.sqlite') filename = File.join(dir, 'cookies.sqlite')