hotswap 0.1.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94b316259e5493780b12f54e304280ebf4e32d1635eee5eae41ffdbbe48400ce
4
- data.tar.gz: dfa1cc6a8905f93e9b24eb9f6527f97601992b8ee7af6ce7b63ee7ff1b714678
3
+ metadata.gz: eb39bbe01190402e38969b9721794ab4845fd46907a4bfd8d5e21f3cd2cc30dc
4
+ data.tar.gz: c948687920739f879999caf87ab23d8baacefbeb8bcd8a008e37976dc0016d39
5
5
  SHA512:
6
- metadata.gz: 7b93ec7dd2f3d2914ddf0280e79579a1c408975353e10a8fd6679c2eb0deafab1685e5db60ddac443d4da2117088f65017dcffad5e50b6256aea79f4440806aa
7
- data.tar.gz: be36ea39ce6ff863689fae39ca946e2b4eb7f33209d65d45d3c824887106bc3f3f4b609db7478197262b153fe4bf48f51d2be98f091e46dec041bc0978ed0d55
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. Use 'database' to refer to the running database."
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
- Use 'database' as a placeholder for the running server's database.
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 database # replace the running database
50
- hotswap cp database ./backup.sqlite3 # snapshot the running database
51
- hotswap cp - database # read from stdin
52
- hotswap cp database - # write to stdout
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
- db_path = Hotswap.database_path
56
- unless db_path
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 src == "database" && dst == "database"
62
- io_err.write("ERROR: source and destination can't both be 'database'\n")
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 dst == "database"
67
- push_database(src, db_path)
68
- elsif src == "database"
69
- pull_database(dst, db_path)
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
- io_err.write("ERROR: one of src/dst must be 'database'\n")
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
- desc "push", "Replace the running database from stdin"
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 push_database(src, db_path)
97
- input = (src == "-") ? io_in : File.open(src, "rb")
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
@@ -3,17 +3,21 @@ module Hotswap
3
3
  config.hotswap = ActiveSupport::OrderedOptions.new
4
4
 
5
5
  initializer "hotswap.configure" do |app|
6
- # Default database path from Rails config
7
- if app.config.hotswap.database_path
8
- Hotswap.database_path = app.config.hotswap.database_path
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
- db_config = app.config.database_configuration[Rails.env]
11
- if db_config && db_config["adapter"]&.include?("sqlite")
12
- Hotswap.database_path = db_config["database"]
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
- # Default socket paths
20
+ # Socket paths
17
21
  if app.config.hotswap.socket_path
18
22
  Hotswap.socket_path = app.config.hotswap.socket_path
19
23
  else
@@ -83,7 +83,6 @@ module Hotswap
83
83
  return unless line
84
84
 
85
85
  parts = Shellwords.split(line.strip)
86
- return if parts.empty?
87
86
 
88
87
  # Grab the stderr socket if one is waiting
89
88
  stderr_io = take_stderr_client
@@ -1,3 +1,3 @@
1
1
  module Hotswap
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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 :database_path, :socket_path, :stderr_socket_path
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.1.4
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