hotswap 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/exe/hotswap +49 -0
- data/lib/hotswap/cli.rb +132 -0
- data/lib/hotswap/middleware.rb +13 -0
- data/lib/hotswap/railtie.rb +41 -0
- data/lib/hotswap/socket_server.rb +127 -0
- data/lib/hotswap/version.rb +3 -0
- data/lib/hotswap.rb +20 -0
- metadata +187 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d4648f76060073d16f095250ed5e638c4150d5094069e839b2bce9b1b471852a
|
|
4
|
+
data.tar.gz: 592b37ce1c1f4691e9bb74d9606ef7727333a3b65f878ebff906084b9a56168e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 869189ee05937fab2b970e31b0cbd13c6a0f36d4d127b0dfb664c793ffd1b060d07050ad0563c1cb224c68187bfa178000305c2cb4e85dad1f0e336d842b69ec
|
|
7
|
+
data.tar.gz: 94f141f2bd7990c6a55a53b01c1491941179453361a014caf91dd2cb83f890026e9898e78b19a8e16f8a8890c175cc298efe1145616900a2eeb9149ffaa503c4
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brad Gessler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/exe/hotswap
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
socket_path = ENV.fetch("HOTSWAP_SOCKET", "tmp/sqlite3.sock")
|
|
7
|
+
stderr_socket_path = ENV.fetch("HOTSWAP_STDERR_SOCKET", "tmp/sqlite3.stderr.sock")
|
|
8
|
+
|
|
9
|
+
unless File.exist?(socket_path)
|
|
10
|
+
$stderr.puts "ERROR: socket not found at #{socket_path}"
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Connect stderr socket first (if available) so it's registered before the command runs
|
|
15
|
+
stderr_sock = nil
|
|
16
|
+
stderr_key = SecureRandom.hex(8)
|
|
17
|
+
if File.exist?(stderr_socket_path)
|
|
18
|
+
stderr_sock = UNIXSocket.new(stderr_socket_path)
|
|
19
|
+
stderr_sock.write("#{stderr_key}\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sock = UNIXSocket.new(socket_path)
|
|
23
|
+
|
|
24
|
+
# Send command args with stderr key as first line
|
|
25
|
+
args = ARGV.join(" ")
|
|
26
|
+
args += " --stderr-key=#{stderr_key}" if stderr_sock
|
|
27
|
+
sock.write(args + "\n")
|
|
28
|
+
|
|
29
|
+
# Pipe stdin to socket (in background thread)
|
|
30
|
+
writer = Thread.new do
|
|
31
|
+
IO.copy_stream($stdin, sock) rescue nil
|
|
32
|
+
sock.close_write rescue nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Pipe stderr socket to local stderr (in background thread)
|
|
36
|
+
stderr_reader = nil
|
|
37
|
+
if stderr_sock
|
|
38
|
+
stderr_reader = Thread.new do
|
|
39
|
+
IO.copy_stream(stderr_sock, $stderr) rescue nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Pipe socket output to stdout
|
|
44
|
+
IO.copy_stream(sock, $stdout) rescue nil
|
|
45
|
+
|
|
46
|
+
writer.join
|
|
47
|
+
stderr_reader&.join
|
|
48
|
+
stderr_sock&.close rescue nil
|
|
49
|
+
sock.close rescue nil
|
data/lib/hotswap/cli.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "tempfile"
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
|
|
5
|
+
module Hotswap
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
def self.exit_on_failure?
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "cp SRC DST", "Copy a database to/from the running server. Use 'database' to refer to the running database."
|
|
12
|
+
long_desc <<~DESC
|
|
13
|
+
Copy a SQLite database to or from the running server.
|
|
14
|
+
|
|
15
|
+
Use 'database' as a placeholder for the running server's database.
|
|
16
|
+
Use '-' for stdin/stdout.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
|
|
20
|
+
hotswap cp ./new.sqlite3 database # replace the running database
|
|
21
|
+
hotswap cp database ./backup.sqlite3 # snapshot the running database
|
|
22
|
+
hotswap cp - database # read from stdin
|
|
23
|
+
hotswap cp database - # write to stdout
|
|
24
|
+
DESC
|
|
25
|
+
def cp(src, dst)
|
|
26
|
+
db_path = Hotswap.database_path
|
|
27
|
+
unless db_path
|
|
28
|
+
$stderr.write("ERROR: database_path not configured\n")
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if src == "database" && dst == "database"
|
|
33
|
+
$stderr.write("ERROR: source and destination can't both be 'database'\n")
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if dst == "database"
|
|
38
|
+
push_database(src, db_path)
|
|
39
|
+
elsif src == "database"
|
|
40
|
+
pull_database(dst, db_path)
|
|
41
|
+
else
|
|
42
|
+
$stderr.write("ERROR: one of src/dst must be 'database'\n")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "push", "Replace the running database from stdin"
|
|
47
|
+
def push
|
|
48
|
+
cp("-", "database")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "pull", "Snapshot the running database to stdout"
|
|
52
|
+
def pull
|
|
53
|
+
cp("database", "-")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "version", "Print the hotswap version"
|
|
57
|
+
def version
|
|
58
|
+
$stdout.write("hotswap #{Hotswap::VERSION}\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def push_database(src, db_path)
|
|
64
|
+
input = (src == "-") ? $stdin : File.open(src, "rb")
|
|
65
|
+
|
|
66
|
+
dir = File.dirname(db_path)
|
|
67
|
+
temp = Tempfile.new(["hotswap", ".sqlite3"], dir)
|
|
68
|
+
begin
|
|
69
|
+
IO.copy_stream(input, temp)
|
|
70
|
+
temp.close
|
|
71
|
+
|
|
72
|
+
db = SQLite3::Database.new(temp.path)
|
|
73
|
+
result = db.execute("PRAGMA integrity_check")
|
|
74
|
+
db.close
|
|
75
|
+
unless result == [["ok"]]
|
|
76
|
+
$stderr.write("ERROR: integrity check failed\n")
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
$stderr.write("Swapping database...\n")
|
|
81
|
+
|
|
82
|
+
Middleware::SWAP_LOCK.synchronize do
|
|
83
|
+
if defined?(ActiveRecord::Base)
|
|
84
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!
|
|
85
|
+
end
|
|
86
|
+
File.rename(temp.path, db_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if defined?(ActiveRecord::Base)
|
|
90
|
+
ActiveRecord::Base.establish_connection
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
$stdout.write("OK\n")
|
|
94
|
+
rescue => e
|
|
95
|
+
$stderr.write("ERROR: #{e.message}\n")
|
|
96
|
+
ensure
|
|
97
|
+
input.close if input.is_a?(File)
|
|
98
|
+
temp.unlink if temp && File.exist?(temp.path)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pull_database(dst, db_path)
|
|
103
|
+
unless File.exist?(db_path)
|
|
104
|
+
$stderr.write("ERROR: database file not found\n")
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
dir = File.dirname(db_path)
|
|
109
|
+
temp = Tempfile.new(["hotswap-pull", ".sqlite3"], dir)
|
|
110
|
+
begin
|
|
111
|
+
src_db = SQLite3::Database.new(db_path)
|
|
112
|
+
dst_db = SQLite3::Database.new(temp.path)
|
|
113
|
+
b = SQLite3::Backup.new(dst_db, "main", src_db, "main")
|
|
114
|
+
b.step(-1)
|
|
115
|
+
b.finish
|
|
116
|
+
dst_db.close
|
|
117
|
+
src_db.close
|
|
118
|
+
|
|
119
|
+
if dst == "-"
|
|
120
|
+
File.open(temp.path, "rb") { |f| IO.copy_stream(f, $stdout) }
|
|
121
|
+
else
|
|
122
|
+
FileUtils.cp(temp.path, dst)
|
|
123
|
+
$stderr.write("OK\n")
|
|
124
|
+
end
|
|
125
|
+
rescue => e
|
|
126
|
+
$stderr.write("ERROR: #{e.message}\n")
|
|
127
|
+
ensure
|
|
128
|
+
temp.unlink if temp && File.exist?(temp.path)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Hotswap
|
|
2
|
+
class Railtie < Rails::Railtie
|
|
3
|
+
config.hotswap = ActiveSupport::OrderedOptions.new
|
|
4
|
+
|
|
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
|
|
9
|
+
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"]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Default socket paths
|
|
17
|
+
if app.config.hotswap.socket_path
|
|
18
|
+
Hotswap.socket_path = app.config.hotswap.socket_path
|
|
19
|
+
else
|
|
20
|
+
Hotswap.socket_path = File.join(app.root, "tmp", "sqlite3.sock")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if app.config.hotswap.stderr_socket_path
|
|
24
|
+
Hotswap.stderr_socket_path = app.config.hotswap.stderr_socket_path
|
|
25
|
+
else
|
|
26
|
+
Hotswap.stderr_socket_path = File.join(app.root, "tmp", "sqlite3.stderr.sock")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
initializer "hotswap.middleware" do |app|
|
|
31
|
+
app.middleware.use Hotswap::Middleware
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
initializer "hotswap.socket_server" do
|
|
35
|
+
server = Hotswap::SocketServer.new
|
|
36
|
+
server.start
|
|
37
|
+
|
|
38
|
+
at_exit { server.stop }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "socket"
|
|
2
|
+
|
|
3
|
+
module Hotswap
|
|
4
|
+
class SocketServer
|
|
5
|
+
attr_reader :socket_path, :stderr_socket_path
|
|
6
|
+
|
|
7
|
+
def initialize(socket_path: Hotswap.socket_path, stderr_socket_path: Hotswap.stderr_socket_path)
|
|
8
|
+
@socket_path = socket_path
|
|
9
|
+
@stderr_socket_path = stderr_socket_path
|
|
10
|
+
@server = nil
|
|
11
|
+
@stderr_server = nil
|
|
12
|
+
@thread = nil
|
|
13
|
+
@stderr_clients = {}
|
|
14
|
+
@stderr_mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start
|
|
18
|
+
cleanup_stale_socket(@socket_path)
|
|
19
|
+
cleanup_stale_socket(@stderr_socket_path)
|
|
20
|
+
|
|
21
|
+
@server = UNIXServer.new(@socket_path)
|
|
22
|
+
@stderr_server = UNIXServer.new(@stderr_socket_path)
|
|
23
|
+
|
|
24
|
+
@thread = Thread.new { accept_loop }
|
|
25
|
+
@thread.report_on_exception = false
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop
|
|
30
|
+
@server&.close
|
|
31
|
+
@stderr_server&.close
|
|
32
|
+
@thread&.kill
|
|
33
|
+
@stderr_mutex.synchronize { @stderr_clients.each_value(&:close) rescue nil }
|
|
34
|
+
[@socket_path, @stderr_socket_path].each do |path|
|
|
35
|
+
File.delete(path) if path && File.exist?(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def accept_loop
|
|
42
|
+
ios = [@server, @stderr_server]
|
|
43
|
+
loop do
|
|
44
|
+
readable, = IO.select(ios)
|
|
45
|
+
readable.each do |server|
|
|
46
|
+
client = server.accept
|
|
47
|
+
if server == @stderr_server
|
|
48
|
+
register_stderr_client(client)
|
|
49
|
+
else
|
|
50
|
+
Thread.new(client) { |sock| handle_connection(sock) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
rescue IOError, Errno::EBADF
|
|
55
|
+
# Server was closed, exit gracefully
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def register_stderr_client(client)
|
|
59
|
+
# stderr client sends its PID on first line so we can match it
|
|
60
|
+
line = client.gets
|
|
61
|
+
return client.close unless line
|
|
62
|
+
key = line.strip
|
|
63
|
+
@stderr_mutex.synchronize { @stderr_clients[key] = client }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def take_stderr_client(key)
|
|
67
|
+
@stderr_mutex.synchronize { @stderr_clients.delete(key) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_connection(socket)
|
|
71
|
+
# First line is the command args
|
|
72
|
+
line = socket.gets
|
|
73
|
+
return unless line
|
|
74
|
+
|
|
75
|
+
parts = line.strip.split(/\s+/)
|
|
76
|
+
# Last part may be --stderr-key=<key>
|
|
77
|
+
stderr_key = nil
|
|
78
|
+
parts.reject! do |p|
|
|
79
|
+
if p.start_with?("--stderr-key=")
|
|
80
|
+
stderr_key = p.split("=", 2).last
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Wait briefly for the stderr client to connect
|
|
86
|
+
stderr_io = nil
|
|
87
|
+
if stderr_key
|
|
88
|
+
5.times do
|
|
89
|
+
stderr_io = take_stderr_client(stderr_key)
|
|
90
|
+
break if stderr_io
|
|
91
|
+
sleep 0.01
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Wire the socket as stdin/stdout and stderr socket as stderr for the CLI
|
|
96
|
+
old_stdin = $stdin
|
|
97
|
+
old_stdout = $stdout
|
|
98
|
+
old_stderr = $stderr
|
|
99
|
+
begin
|
|
100
|
+
$stdin = socket
|
|
101
|
+
$stdout = socket
|
|
102
|
+
$stderr = stderr_io || $stderr
|
|
103
|
+
Hotswap::CLI.start(parts)
|
|
104
|
+
ensure
|
|
105
|
+
$stdin = old_stdin
|
|
106
|
+
$stdout = old_stdout
|
|
107
|
+
$stderr = old_stderr
|
|
108
|
+
end
|
|
109
|
+
rescue => e
|
|
110
|
+
socket.write("ERROR: #{e.message}\n") rescue nil
|
|
111
|
+
ensure
|
|
112
|
+
stderr_io&.close rescue nil
|
|
113
|
+
socket.close rescue nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cleanup_stale_socket(path)
|
|
117
|
+
return unless File.exist?(path)
|
|
118
|
+
begin
|
|
119
|
+
test = UNIXSocket.new(path)
|
|
120
|
+
test.close
|
|
121
|
+
raise Hotswap::Error, "Socket #{path} is already in use"
|
|
122
|
+
rescue Errno::ECONNREFUSED, Errno::ENOENT
|
|
123
|
+
File.delete(path)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/hotswap.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative "hotswap/version"
|
|
2
|
+
require_relative "hotswap/middleware"
|
|
3
|
+
require_relative "hotswap/cli"
|
|
4
|
+
require_relative "hotswap/socket_server"
|
|
5
|
+
require_relative "hotswap/railtie" if defined?(Rails::Railtie)
|
|
6
|
+
|
|
7
|
+
module Hotswap
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_accessor :database_path, :socket_path, :stderr_socket_path
|
|
12
|
+
|
|
13
|
+
def configure
|
|
14
|
+
yield self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
self.socket_path = "tmp/sqlite3.sock"
|
|
19
|
+
self.stderr_socket_path = "tmp/sqlite3.stderr.sock"
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hotswap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brad Gessler
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-02-27 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sqlite3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rack-test
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rake
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: railties
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: activerecord
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: actionpack
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: webrick
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
description: Swap a SQLite database on a running Rails server without restart. Requests
|
|
153
|
+
queue briefly during the swap, then resume on the new database.
|
|
154
|
+
executables:
|
|
155
|
+
- hotswap
|
|
156
|
+
extensions: []
|
|
157
|
+
extra_rdoc_files: []
|
|
158
|
+
files:
|
|
159
|
+
- LICENSE
|
|
160
|
+
- exe/hotswap
|
|
161
|
+
- lib/hotswap.rb
|
|
162
|
+
- lib/hotswap/cli.rb
|
|
163
|
+
- lib/hotswap/middleware.rb
|
|
164
|
+
- lib/hotswap/railtie.rb
|
|
165
|
+
- lib/hotswap/socket_server.rb
|
|
166
|
+
- lib/hotswap/version.rb
|
|
167
|
+
licenses:
|
|
168
|
+
- MIT
|
|
169
|
+
metadata: {}
|
|
170
|
+
rdoc_options: []
|
|
171
|
+
require_paths:
|
|
172
|
+
- lib
|
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
|
+
requirements:
|
|
175
|
+
- - ">="
|
|
176
|
+
- !ruby/object:Gem::Version
|
|
177
|
+
version: '3.0'
|
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
|
+
requirements:
|
|
180
|
+
- - ">="
|
|
181
|
+
- !ruby/object:Gem::Version
|
|
182
|
+
version: '0'
|
|
183
|
+
requirements: []
|
|
184
|
+
rubygems_version: 3.6.2
|
|
185
|
+
specification_version: 4
|
|
186
|
+
summary: Hot-swap SQLite databases on a running Rails server
|
|
187
|
+
test_files: []
|