hotswap 0.1.5 → 0.2.0
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/lib/hotswap/cli.rb +23 -102
- data/lib/hotswap/database.rb +88 -0
- data/lib/hotswap/railtie.rb +11 -7
- data/lib/hotswap/version.rb +1 -1
- data/lib/hotswap.rb +29 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb39bbe01190402e38969b9721794ab4845fd46907a4bfd8d5e21f3cd2cc30dc
|
|
4
|
+
data.tar.gz: c948687920739f879999caf87ab23d8baacefbeb8bcd8a008e37976dc0016d39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f549bb47a1e79ee364b68cb8e96160f877190d41f8bdf785ea16659d1f7b5aad3bacd052085c588b9b089906d152bc0428098729d5a44b61c7e1cec866680b29
|
|
7
|
+
data.tar.gz: daaaf30d063d08ced91ff9249f625a6424d50247e1748f2b75506ce21bf6279fce63a9001adf72649397bcd3d45d375aafa498d4f94d45e2449d05797daba2b7
|
data/lib/hotswap/cli.rb
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
require "thor"
|
|
2
|
-
require "tempfile"
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "sqlite3"
|
|
5
2
|
|
|
6
3
|
module Hotswap
|
|
7
4
|
class CLI < Thor
|
|
@@ -20,8 +17,6 @@ module Hotswap
|
|
|
20
17
|
def stderr = @_stderr
|
|
21
18
|
end
|
|
22
19
|
|
|
23
|
-
# Thread-safe IO: each connection gets its own IO via thread-local storage
|
|
24
|
-
# instead of swapping global $stdin/$stdout/$stderr.
|
|
25
20
|
def self.run(args, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
26
21
|
Thread.current[:hotswap_stdin] = stdin
|
|
27
22
|
Thread.current[:hotswap_stdout] = stdout
|
|
@@ -37,52 +32,42 @@ module Hotswap
|
|
|
37
32
|
Thread.current[:hotswap_stderr] = nil
|
|
38
33
|
end
|
|
39
34
|
|
|
40
|
-
desc "cp SRC DST", "Copy a database to/from the running server
|
|
35
|
+
desc "cp SRC DST", "Copy a database to/from the running server"
|
|
41
36
|
long_desc <<~DESC
|
|
42
37
|
Copy a SQLite database to or from the running server.
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
Use '-' for stdin/stdout.
|
|
39
|
+
If SRC or DST matches a managed database path, hotswap treats it as
|
|
40
|
+
the live database. Use '-' for stdin/stdout.
|
|
46
41
|
|
|
47
42
|
Examples:
|
|
48
43
|
|
|
49
|
-
hotswap cp ./new.sqlite3
|
|
50
|
-
hotswap cp
|
|
51
|
-
hotswap cp -
|
|
52
|
-
hotswap cp
|
|
44
|
+
hotswap cp ./new.sqlite3 db/production.sqlite3 # push
|
|
45
|
+
hotswap cp db/production.sqlite3 ./backup.sqlite3 # pull
|
|
46
|
+
hotswap cp - db/production.sqlite3 # push from stdin
|
|
47
|
+
hotswap cp db/production.sqlite3 - # pull to stdout
|
|
53
48
|
DESC
|
|
54
49
|
def cp(src, dst)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
io_err.write("ERROR: database_path not configured\n")
|
|
58
|
-
return
|
|
59
|
-
end
|
|
50
|
+
src_db = resolve_database(src)
|
|
51
|
+
dst_db = resolve_database(dst)
|
|
60
52
|
|
|
61
|
-
if
|
|
62
|
-
io_err.write("ERROR: source and destination can't both be
|
|
53
|
+
if src_db && dst_db
|
|
54
|
+
io_err.write("ERROR: source and destination can't both be managed databases\n")
|
|
63
55
|
return
|
|
64
56
|
end
|
|
65
57
|
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
if dst_db
|
|
59
|
+
source = (src == "-") ? io_in : src
|
|
60
|
+
dst_db.push(source, stdout: io_out, stderr: io_err)
|
|
61
|
+
elsif src_db
|
|
62
|
+
destination = (dst == "-") ? io_out : dst
|
|
63
|
+
src_db.pull(destination, stderr: io_err)
|
|
70
64
|
else
|
|
71
|
-
|
|
65
|
+
paths = Hotswap.databases.map(&:path).join(", ")
|
|
66
|
+
io_err.write("ERROR: neither path matches a managed database (#{paths})\n")
|
|
72
67
|
end
|
|
73
68
|
end
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
def push
|
|
77
|
-
cp("-", "database")
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
desc "pull", "Snapshot the running database to stdout"
|
|
81
|
-
def pull
|
|
82
|
-
cp("database", "-")
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
desc "version", "Print the hotswap version"
|
|
70
|
+
desc "version", "Print the hotswap version"
|
|
86
71
|
def version
|
|
87
72
|
io_out.write("hotswap #{Hotswap::VERSION}\n")
|
|
88
73
|
end
|
|
@@ -93,73 +78,9 @@ module Hotswap
|
|
|
93
78
|
def io_out = Thread.current[:hotswap_stdout] || $stdout
|
|
94
79
|
def io_err = Thread.current[:hotswap_stderr] || $stderr
|
|
95
80
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
dir = File.dirname(db_path)
|
|
100
|
-
temp = Tempfile.new(["hotswap", ".sqlite3"], dir)
|
|
101
|
-
begin
|
|
102
|
-
IO.copy_stream(input, temp)
|
|
103
|
-
temp.close
|
|
104
|
-
|
|
105
|
-
db = SQLite3::Database.new(temp.path)
|
|
106
|
-
result = db.execute("PRAGMA integrity_check")
|
|
107
|
-
db.close
|
|
108
|
-
unless result == [["ok"]]
|
|
109
|
-
io_err.write("ERROR: integrity check failed\n")
|
|
110
|
-
return
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
io_err.write("Swapping database...\n")
|
|
114
|
-
|
|
115
|
-
Middleware::SWAP_LOCK.synchronize do
|
|
116
|
-
if defined?(ActiveRecord::Base)
|
|
117
|
-
ActiveRecord::Base.connection_handler.clear_all_connections!
|
|
118
|
-
end
|
|
119
|
-
File.rename(temp.path, db_path)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
if defined?(ActiveRecord::Base)
|
|
123
|
-
ActiveRecord::Base.establish_connection
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
io_out.write("OK\n")
|
|
127
|
-
rescue => e
|
|
128
|
-
io_err.write("ERROR: #{e.message}\n")
|
|
129
|
-
ensure
|
|
130
|
-
input.close if input.is_a?(File)
|
|
131
|
-
temp.unlink if temp && File.exist?(temp.path)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def pull_database(dst, db_path)
|
|
136
|
-
unless File.exist?(db_path)
|
|
137
|
-
io_err.write("ERROR: database file not found\n")
|
|
138
|
-
return
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
dir = File.dirname(db_path)
|
|
142
|
-
temp = Tempfile.new(["hotswap-pull", ".sqlite3"], dir)
|
|
143
|
-
begin
|
|
144
|
-
src_db = SQLite3::Database.new(db_path)
|
|
145
|
-
dst_db = SQLite3::Database.new(temp.path)
|
|
146
|
-
b = SQLite3::Backup.new(dst_db, "main", src_db, "main")
|
|
147
|
-
b.step(-1)
|
|
148
|
-
b.finish
|
|
149
|
-
dst_db.close
|
|
150
|
-
src_db.close
|
|
151
|
-
|
|
152
|
-
if dst == "-"
|
|
153
|
-
File.open(temp.path, "rb") { |f| IO.copy_stream(f, io_out) }
|
|
154
|
-
else
|
|
155
|
-
FileUtils.cp(temp.path, dst)
|
|
156
|
-
io_err.write("OK\n")
|
|
157
|
-
end
|
|
158
|
-
rescue => e
|
|
159
|
-
io_err.write("ERROR: #{e.message}\n")
|
|
160
|
-
ensure
|
|
161
|
-
temp.unlink if temp && File.exist?(temp.path)
|
|
162
|
-
end
|
|
81
|
+
def resolve_database(path)
|
|
82
|
+
return nil if path == "-"
|
|
83
|
+
Hotswap.find_database(path) || (path == "database" && Hotswap.databases.first)
|
|
163
84
|
end
|
|
164
85
|
end
|
|
165
86
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
|
|
5
|
+
module Hotswap
|
|
6
|
+
class Database
|
|
7
|
+
attr_reader :path
|
|
8
|
+
|
|
9
|
+
def initialize(path)
|
|
10
|
+
@path = File.expand_path(path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Push a new database from an IO stream or file path
|
|
14
|
+
def push(source, stdout: $stdout, stderr: $stderr)
|
|
15
|
+
input = source.is_a?(String) ? File.open(source, "rb") : source
|
|
16
|
+
|
|
17
|
+
dir = File.dirname(@path)
|
|
18
|
+
temp = Tempfile.new(["hotswap", ".sqlite3"], dir)
|
|
19
|
+
begin
|
|
20
|
+
IO.copy_stream(input, temp)
|
|
21
|
+
temp.close
|
|
22
|
+
|
|
23
|
+
db = SQLite3::Database.new(temp.path)
|
|
24
|
+
result = db.execute("PRAGMA integrity_check")
|
|
25
|
+
db.close
|
|
26
|
+
unless result == [["ok"]]
|
|
27
|
+
stderr.write("ERROR: integrity check failed\n")
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
stderr.write("Swapping database...\n")
|
|
32
|
+
|
|
33
|
+
Middleware::SWAP_LOCK.synchronize do
|
|
34
|
+
if defined?(ActiveRecord::Base)
|
|
35
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!
|
|
36
|
+
end
|
|
37
|
+
File.rename(temp.path, @path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if defined?(ActiveRecord::Base)
|
|
41
|
+
ActiveRecord::Base.establish_connection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
stdout.write("OK\n")
|
|
45
|
+
true
|
|
46
|
+
rescue => e
|
|
47
|
+
stderr.write("ERROR: #{e.message}\n")
|
|
48
|
+
false
|
|
49
|
+
ensure
|
|
50
|
+
input.close if source.is_a?(String) && input.is_a?(File)
|
|
51
|
+
temp.unlink if temp && File.exist?(temp.path)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Pull the database to an IO stream or file path
|
|
56
|
+
def pull(destination, stderr: $stderr)
|
|
57
|
+
unless File.exist?(@path)
|
|
58
|
+
stderr.write("ERROR: database file not found at #{@path}\n")
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
dir = File.dirname(@path)
|
|
63
|
+
temp = Tempfile.new(["hotswap-pull", ".sqlite3"], dir)
|
|
64
|
+
begin
|
|
65
|
+
src_db = SQLite3::Database.new(@path)
|
|
66
|
+
dst_db = SQLite3::Database.new(temp.path)
|
|
67
|
+
b = SQLite3::Backup.new(dst_db, "main", src_db, "main")
|
|
68
|
+
b.step(-1)
|
|
69
|
+
b.finish
|
|
70
|
+
dst_db.close
|
|
71
|
+
src_db.close
|
|
72
|
+
|
|
73
|
+
if destination.is_a?(String)
|
|
74
|
+
FileUtils.cp(temp.path, destination)
|
|
75
|
+
stderr.write("OK\n")
|
|
76
|
+
else
|
|
77
|
+
File.open(temp.path, "rb") { |f| IO.copy_stream(f, destination) }
|
|
78
|
+
end
|
|
79
|
+
true
|
|
80
|
+
rescue => e
|
|
81
|
+
stderr.write("ERROR: #{e.message}\n")
|
|
82
|
+
false
|
|
83
|
+
ensure
|
|
84
|
+
temp.unlink if temp && File.exist?(temp.path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/hotswap/railtie.rb
CHANGED
|
@@ -3,17 +3,21 @@ module Hotswap
|
|
|
3
3
|
config.hotswap = ActiveSupport::OrderedOptions.new
|
|
4
4
|
|
|
5
5
|
initializer "hotswap.configure" do |app|
|
|
6
|
-
#
|
|
7
|
-
if app.config.hotswap.
|
|
8
|
-
|
|
6
|
+
# Discover all SQLite databases from Rails config
|
|
7
|
+
if app.config.hotswap.database_paths
|
|
8
|
+
Array(app.config.hotswap.database_paths).each { |p| Hotswap.register(p) }
|
|
9
|
+
elsif app.config.hotswap.database_path
|
|
10
|
+
Hotswap.register(app.config.hotswap.database_path)
|
|
9
11
|
else
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
|
|
13
|
+
db_configs.each do |db_config|
|
|
14
|
+
if db_config.adapter.include?("sqlite")
|
|
15
|
+
Hotswap.register(db_config.database)
|
|
16
|
+
end
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
#
|
|
20
|
+
# Socket paths
|
|
17
21
|
if app.config.hotswap.socket_path
|
|
18
22
|
Hotswap.socket_path = app.config.hotswap.socket_path
|
|
19
23
|
else
|
data/lib/hotswap/version.rb
CHANGED
data/lib/hotswap.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "hotswap/version"
|
|
2
2
|
require_relative "hotswap/middleware"
|
|
3
|
+
require_relative "hotswap/database"
|
|
3
4
|
require_relative "hotswap/cli"
|
|
4
5
|
require_relative "hotswap/socket_server"
|
|
5
6
|
require_relative "hotswap/railtie" if defined?(Rails::Railtie)
|
|
@@ -8,11 +9,38 @@ module Hotswap
|
|
|
8
9
|
class Error < StandardError; end
|
|
9
10
|
|
|
10
11
|
class << self
|
|
11
|
-
attr_accessor :
|
|
12
|
+
attr_accessor :socket_path, :stderr_socket_path
|
|
12
13
|
|
|
13
14
|
def configure
|
|
14
15
|
yield self
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
# Registry of managed databases
|
|
19
|
+
def databases
|
|
20
|
+
@databases ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def register(path)
|
|
24
|
+
db = Database.new(path)
|
|
25
|
+
databases << db unless databases.any? { |d| d.path == db.path }
|
|
26
|
+
db
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find_database(path)
|
|
30
|
+
return nil if path == "-"
|
|
31
|
+
resolved = File.expand_path(path)
|
|
32
|
+
databases.find { |db| db.path == resolved }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Backward compat: single database_path getter/setter
|
|
36
|
+
def database_path=(path)
|
|
37
|
+
@databases = []
|
|
38
|
+
register(path) if path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def database_path
|
|
42
|
+
databases.first&.path
|
|
43
|
+
end
|
|
16
44
|
end
|
|
17
45
|
|
|
18
46
|
self.socket_path = "tmp/sockets/hotswap.sock"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hotswap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brad Gessler
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- exe/hotswap
|
|
161
161
|
- lib/hotswap.rb
|
|
162
162
|
- lib/hotswap/cli.rb
|
|
163
|
+
- lib/hotswap/database.rb
|
|
163
164
|
- lib/hotswap/middleware.rb
|
|
164
165
|
- lib/hotswap/railtie.rb
|
|
165
166
|
- lib/hotswap/socket_server.rb
|