hotswap 0.1.0 → 0.1.2
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/exe/hotswap +6 -9
- data/lib/hotswap/cli.rb +31 -13
- data/lib/hotswap/railtie.rb +3 -3
- data/lib/hotswap/socket_server.rb +34 -46
- data/lib/hotswap/version.rb +1 -1
- data/lib/hotswap.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31ba7d63578622f3d225e863e638be171c949af98f6497019a3ce01c7371c71d
|
|
4
|
+
data.tar.gz: 772a73f952822313044f1a28d1b5013458eafa0685772f97bdefe41372ed933b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7c028345800763facf9a6a76c17d34f5b3e8c6aa930778f76f28e731248024a67c9f0b6fba6d548aa6562459c8a2c00d76aa4f4b2d3815fdf87c81e9518e2d1
|
|
7
|
+
data.tar.gz: e4c172d547d26c443030c720e537f95bfad0232f7a28f72458e8fb367f7b4f1c54d7a840f6c825c1bdf1892690e4e3e1c4e72e3c438b57eafa8ff9513793539f
|
data/exe/hotswap
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
-
require "
|
|
4
|
+
require "shellwords"
|
|
5
5
|
|
|
6
|
-
socket_path = ENV.fetch("HOTSWAP_SOCKET", "tmp/
|
|
7
|
-
stderr_socket_path = ENV.fetch("HOTSWAP_STDERR_SOCKET", "tmp/
|
|
6
|
+
socket_path = ENV.fetch("HOTSWAP_SOCKET", "tmp/sockets/hotswap.sock")
|
|
7
|
+
stderr_socket_path = ENV.fetch("HOTSWAP_STDERR_SOCKET", "tmp/sockets/hotswap.stderr.sock")
|
|
8
8
|
|
|
9
9
|
unless File.exist?(socket_path)
|
|
10
10
|
$stderr.puts "ERROR: socket not found at #{socket_path}"
|
|
11
11
|
exit 1
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# Connect stderr socket first
|
|
14
|
+
# Connect stderr socket first so it's ready before the command runs
|
|
15
15
|
stderr_sock = nil
|
|
16
|
-
stderr_key = SecureRandom.hex(8)
|
|
17
16
|
if File.exist?(stderr_socket_path)
|
|
18
17
|
stderr_sock = UNIXSocket.new(stderr_socket_path)
|
|
19
|
-
stderr_sock.write("#{stderr_key}\n")
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
sock = UNIXSocket.new(socket_path)
|
|
23
21
|
|
|
24
|
-
#
|
|
25
|
-
args = ARGV.join(" ")
|
|
26
|
-
args += " --stderr-key=#{stderr_key}" if stderr_sock
|
|
22
|
+
# Shell-escape args to preserve spaces in file paths
|
|
23
|
+
args = ARGV.map { |a| Shellwords.escape(a) }.join(" ")
|
|
27
24
|
sock.write(args + "\n")
|
|
28
25
|
|
|
29
26
|
# Pipe stdin to socket (in background thread)
|
data/lib/hotswap/cli.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "thor"
|
|
2
2
|
require "tempfile"
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "sqlite3"
|
|
4
5
|
|
|
5
6
|
module Hotswap
|
|
@@ -8,6 +9,19 @@ module Hotswap
|
|
|
8
9
|
true
|
|
9
10
|
end
|
|
10
11
|
|
|
12
|
+
# Thread-safe IO: each connection gets its own IO via thread-local storage
|
|
13
|
+
# instead of swapping global $stdin/$stdout/$stderr.
|
|
14
|
+
def self.run(args, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
15
|
+
Thread.current[:hotswap_stdin] = stdin
|
|
16
|
+
Thread.current[:hotswap_stdout] = stdout
|
|
17
|
+
Thread.current[:hotswap_stderr] = stderr
|
|
18
|
+
start(args)
|
|
19
|
+
ensure
|
|
20
|
+
Thread.current[:hotswap_stdin] = nil
|
|
21
|
+
Thread.current[:hotswap_stdout] = nil
|
|
22
|
+
Thread.current[:hotswap_stderr] = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
11
25
|
desc "cp SRC DST", "Copy a database to/from the running server. Use 'database' to refer to the running database."
|
|
12
26
|
long_desc <<~DESC
|
|
13
27
|
Copy a SQLite database to or from the running server.
|
|
@@ -25,12 +39,12 @@ module Hotswap
|
|
|
25
39
|
def cp(src, dst)
|
|
26
40
|
db_path = Hotswap.database_path
|
|
27
41
|
unless db_path
|
|
28
|
-
|
|
42
|
+
io_err.write("ERROR: database_path not configured\n")
|
|
29
43
|
return
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
if src == "database" && dst == "database"
|
|
33
|
-
|
|
47
|
+
io_err.write("ERROR: source and destination can't both be 'database'\n")
|
|
34
48
|
return
|
|
35
49
|
end
|
|
36
50
|
|
|
@@ -39,7 +53,7 @@ module Hotswap
|
|
|
39
53
|
elsif src == "database"
|
|
40
54
|
pull_database(dst, db_path)
|
|
41
55
|
else
|
|
42
|
-
|
|
56
|
+
io_err.write("ERROR: one of src/dst must be 'database'\n")
|
|
43
57
|
end
|
|
44
58
|
end
|
|
45
59
|
|
|
@@ -55,13 +69,17 @@ module Hotswap
|
|
|
55
69
|
|
|
56
70
|
desc "version", "Print the hotswap version"
|
|
57
71
|
def version
|
|
58
|
-
|
|
72
|
+
io_out.write("hotswap #{Hotswap::VERSION}\n")
|
|
59
73
|
end
|
|
60
74
|
|
|
61
75
|
private
|
|
62
76
|
|
|
77
|
+
def io_in = Thread.current[:hotswap_stdin] || $stdin
|
|
78
|
+
def io_out = Thread.current[:hotswap_stdout] || $stdout
|
|
79
|
+
def io_err = Thread.current[:hotswap_stderr] || $stderr
|
|
80
|
+
|
|
63
81
|
def push_database(src, db_path)
|
|
64
|
-
input = (src == "-") ?
|
|
82
|
+
input = (src == "-") ? io_in : File.open(src, "rb")
|
|
65
83
|
|
|
66
84
|
dir = File.dirname(db_path)
|
|
67
85
|
temp = Tempfile.new(["hotswap", ".sqlite3"], dir)
|
|
@@ -73,11 +91,11 @@ module Hotswap
|
|
|
73
91
|
result = db.execute("PRAGMA integrity_check")
|
|
74
92
|
db.close
|
|
75
93
|
unless result == [["ok"]]
|
|
76
|
-
|
|
94
|
+
io_err.write("ERROR: integrity check failed\n")
|
|
77
95
|
return
|
|
78
96
|
end
|
|
79
97
|
|
|
80
|
-
|
|
98
|
+
io_err.write("Swapping database...\n")
|
|
81
99
|
|
|
82
100
|
Middleware::SWAP_LOCK.synchronize do
|
|
83
101
|
if defined?(ActiveRecord::Base)
|
|
@@ -90,9 +108,9 @@ module Hotswap
|
|
|
90
108
|
ActiveRecord::Base.establish_connection
|
|
91
109
|
end
|
|
92
110
|
|
|
93
|
-
|
|
111
|
+
io_out.write("OK\n")
|
|
94
112
|
rescue => e
|
|
95
|
-
|
|
113
|
+
io_err.write("ERROR: #{e.message}\n")
|
|
96
114
|
ensure
|
|
97
115
|
input.close if input.is_a?(File)
|
|
98
116
|
temp.unlink if temp && File.exist?(temp.path)
|
|
@@ -101,7 +119,7 @@ module Hotswap
|
|
|
101
119
|
|
|
102
120
|
def pull_database(dst, db_path)
|
|
103
121
|
unless File.exist?(db_path)
|
|
104
|
-
|
|
122
|
+
io_err.write("ERROR: database file not found\n")
|
|
105
123
|
return
|
|
106
124
|
end
|
|
107
125
|
|
|
@@ -117,13 +135,13 @@ module Hotswap
|
|
|
117
135
|
src_db.close
|
|
118
136
|
|
|
119
137
|
if dst == "-"
|
|
120
|
-
File.open(temp.path, "rb") { |f| IO.copy_stream(f,
|
|
138
|
+
File.open(temp.path, "rb") { |f| IO.copy_stream(f, io_out) }
|
|
121
139
|
else
|
|
122
140
|
FileUtils.cp(temp.path, dst)
|
|
123
|
-
|
|
141
|
+
io_err.write("OK\n")
|
|
124
142
|
end
|
|
125
143
|
rescue => e
|
|
126
|
-
|
|
144
|
+
io_err.write("ERROR: #{e.message}\n")
|
|
127
145
|
ensure
|
|
128
146
|
temp.unlink if temp && File.exist?(temp.path)
|
|
129
147
|
end
|
data/lib/hotswap/railtie.rb
CHANGED
|
@@ -17,13 +17,13 @@ module Hotswap
|
|
|
17
17
|
if app.config.hotswap.socket_path
|
|
18
18
|
Hotswap.socket_path = app.config.hotswap.socket_path
|
|
19
19
|
else
|
|
20
|
-
Hotswap.socket_path = File.join(app.root, "tmp", "
|
|
20
|
+
Hotswap.socket_path = File.join(app.root, "tmp", "sockets", "hotswap.sock")
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
if app.config.hotswap.stderr_socket_path
|
|
24
24
|
Hotswap.stderr_socket_path = app.config.hotswap.stderr_socket_path
|
|
25
25
|
else
|
|
26
|
-
Hotswap.stderr_socket_path = File.join(app.root, "tmp", "
|
|
26
|
+
Hotswap.stderr_socket_path = File.join(app.root, "tmp", "sockets", "hotswap.stderr.sock")
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ module Hotswap
|
|
|
31
31
|
app.middleware.use Hotswap::Middleware
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
server do
|
|
35
35
|
server = Hotswap::SocketServer.new
|
|
36
36
|
server.start
|
|
37
37
|
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
require "socket"
|
|
2
|
+
require "shellwords"
|
|
2
3
|
|
|
3
4
|
module Hotswap
|
|
4
5
|
class SocketServer
|
|
5
6
|
attr_reader :socket_path, :stderr_socket_path
|
|
6
7
|
|
|
8
|
+
CONNECTION_TIMEOUT = 10
|
|
9
|
+
|
|
7
10
|
def initialize(socket_path: Hotswap.socket_path, stderr_socket_path: Hotswap.stderr_socket_path)
|
|
8
11
|
@socket_path = socket_path
|
|
9
12
|
@stderr_socket_path = stderr_socket_path
|
|
10
13
|
@server = nil
|
|
11
14
|
@stderr_server = nil
|
|
12
15
|
@thread = nil
|
|
13
|
-
@
|
|
16
|
+
@stderr_client = nil
|
|
14
17
|
@stderr_mutex = Mutex.new
|
|
15
18
|
end
|
|
16
19
|
|
|
@@ -18,6 +21,7 @@ module Hotswap
|
|
|
18
21
|
cleanup_stale_socket(@socket_path)
|
|
19
22
|
cleanup_stale_socket(@stderr_socket_path)
|
|
20
23
|
|
|
24
|
+
FileUtils.mkdir_p(File.dirname(@socket_path))
|
|
21
25
|
@server = UNIXServer.new(@socket_path)
|
|
22
26
|
@stderr_server = UNIXServer.new(@stderr_socket_path)
|
|
23
27
|
|
|
@@ -30,7 +34,10 @@ module Hotswap
|
|
|
30
34
|
@server&.close
|
|
31
35
|
@stderr_server&.close
|
|
32
36
|
@thread&.kill
|
|
33
|
-
@stderr_mutex.synchronize
|
|
37
|
+
@stderr_mutex.synchronize do
|
|
38
|
+
@stderr_client&.close rescue nil
|
|
39
|
+
@stderr_client = nil
|
|
40
|
+
end
|
|
34
41
|
[@socket_path, @stderr_socket_path].each do |path|
|
|
35
42
|
File.delete(path) if path && File.exist?(path)
|
|
36
43
|
end
|
|
@@ -45,7 +52,10 @@ module Hotswap
|
|
|
45
52
|
readable.each do |server|
|
|
46
53
|
client = server.accept
|
|
47
54
|
if server == @stderr_server
|
|
48
|
-
|
|
55
|
+
@stderr_mutex.synchronize do
|
|
56
|
+
@stderr_client&.close rescue nil
|
|
57
|
+
@stderr_client = client
|
|
58
|
+
end
|
|
49
59
|
else
|
|
50
60
|
Thread.new(client) { |sock| handle_connection(sock) }
|
|
51
61
|
end
|
|
@@ -55,57 +65,35 @@ module Hotswap
|
|
|
55
65
|
# Server was closed, exit gracefully
|
|
56
66
|
end
|
|
57
67
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def take_stderr_client(key)
|
|
67
|
-
@stderr_mutex.synchronize { @stderr_clients.delete(key) }
|
|
68
|
+
def take_stderr_client
|
|
69
|
+
@stderr_mutex.synchronize do
|
|
70
|
+
client = @stderr_client
|
|
71
|
+
@stderr_client = nil
|
|
72
|
+
client
|
|
73
|
+
end
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def handle_connection(socket)
|
|
71
|
-
|
|
77
|
+
unless IO.select([socket], nil, nil, CONNECTION_TIMEOUT)
|
|
78
|
+
socket.write("ERROR: connection timeout\n") rescue nil
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
72
82
|
line = socket.gets
|
|
73
83
|
return unless line
|
|
74
84
|
|
|
75
|
-
parts = line.strip
|
|
76
|
-
|
|
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
|
|
85
|
+
parts = Shellwords.split(line.strip)
|
|
86
|
+
return if parts.empty?
|
|
84
87
|
|
|
85
|
-
#
|
|
86
|
-
stderr_io =
|
|
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
|
|
88
|
+
# Grab the stderr socket if one is waiting
|
|
89
|
+
stderr_io = take_stderr_client
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
91
|
+
Hotswap::CLI.run(
|
|
92
|
+
parts,
|
|
93
|
+
stdin: socket,
|
|
94
|
+
stdout: socket,
|
|
95
|
+
stderr: stderr_io || $stderr
|
|
96
|
+
)
|
|
109
97
|
rescue => e
|
|
110
98
|
socket.write("ERROR: #{e.message}\n") rescue nil
|
|
111
99
|
ensure
|
data/lib/hotswap/version.rb
CHANGED
data/lib/hotswap.rb
CHANGED