WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit a771bd8

Browse files
aggressive unix socket cleanup handling
1 parent ff697f2 commit a771bd8

File tree

4 files changed

+233
-17
lines changed

4 files changed

+233
-17
lines changed

bin/bench-server.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5+
6+
puts "Loading hyper_ruby"
7+
8+
require "hyper_ruby"
9+
require "json"
10+
11+
# Create and configure the server
12+
server = HyperRuby::Server.new
13+
config = {
14+
bind_address: ENV.fetch("BIND_ADDRESS", "127.0.0.1:3000"),
15+
tokio_threads: ENV.fetch("TOKIO_THREADS", "1").to_i,
16+
debug: ENV.fetch("DEBUG", "false") == "true",
17+
recv_timeout: ENV.fetch("RECV_TIMEOUT", "30000").to_i
18+
}
19+
server.configure(config)
20+
21+
puts "Starting server with config: #{config}"
22+
23+
accept_response = HyperRuby::Response.new(
24+
200,
25+
{ "Content-Type" => "application/json" },
26+
{ "message" => "Accepted" }.to_json
27+
)
28+
29+
# Start the server
30+
server.start
31+
32+
puts "Server started"
33+
34+
# Create a worker thread to handle requests
35+
worker = Thread.new do
36+
server.run_worker do |request|
37+
# read the body into a buffer, to simulate some work
38+
buffer = String.new(capacity: 1024)
39+
request.fill_body(buffer)
40+
41+
accept_response
42+
end
43+
end
44+
45+
puts "Server running at #{config[:bind_address]}"
46+
puts "Press Ctrl+C to stop"
47+
48+
# Wait for Ctrl+C
49+
begin
50+
sleep
51+
rescue Interrupt
52+
puts "\nShutting down..."
53+
server.stop
54+
worker.join
55+
end

bin/run-server.rb

100644100755
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
end
4848
end
4949

50-
puts "Server running at #{config[:bind_address]}"
50+
puts "Server running at #{server.config.bind_address}"
5151
puts "Press Ctrl+C to stop"
5252

5353
# Wait for Ctrl+C

ext/hyper_ruby/src/lib.rs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -260,23 +260,72 @@ impl Server {
260260

261261

262262
rt.block_on(async move {
263-
let server_task = tokio::spawn(async move {
264-
let timer = hyper_util::rt::TokioTimer::new();
265-
let mut builder = auto::Builder::new(hyper_util::rt::TokioExecutor::new());
266-
builder.http1()
267-
.header_read_timeout(std::time::Duration::from_millis(config.recv_timeout))
268-
.timer(timer.clone());
269-
builder.http2()
270-
.keep_alive_interval(std::time::Duration::from_secs(10))
271-
.timer(timer);
272-
273-
let listener = if config.bind_address.starts_with("unix:") {
274-
Listener::Unix(UnixListener::bind(config.bind_address.trim_start_matches("unix:")).unwrap())
275-
} else {
276-
let addr: SocketAddr = config.bind_address.parse().expect("invalid address format");
277-
Listener::Tcp(TcpListener::bind(addr).await.unwrap())
278-
};
263+
// Instead of spawning a task, we'll run the server setup inline first to catch binding errors
264+
// Setup listener and http server components
265+
let timer = hyper_util::rt::TokioTimer::new();
266+
let mut builder = auto::Builder::new(hyper_util::rt::TokioExecutor::new());
267+
builder.http1()
268+
.header_read_timeout(std::time::Duration::from_millis(config.recv_timeout))
269+
.timer(timer.clone());
270+
builder.http2()
271+
.keep_alive_interval(std::time::Duration::from_secs(10))
272+
.timer(timer);
273+
274+
// Create the listener with proper error handling
275+
let listener = if config.bind_address.starts_with("unix:") {
276+
let path = config.bind_address.trim_start_matches("unix:");
277+
278+
// Check if the socket file already exists and try to delete it
279+
if std::path::Path::new(path).exists() {
280+
debug!("Unix socket file {} already exists, attempting to remove it", path);
281+
match std::fs::remove_file(path) {
282+
Ok(_) => debug!("Successfully removed existing socket file"),
283+
Err(e) => {
284+
error!("Failed to remove existing Unix socket file {}: {}", path, e);
285+
return Err(MagnusError::new(
286+
magnus::exception::runtime_error(),
287+
format!("Failed to remove existing Unix socket file {}: {}", path, e)
288+
));
289+
}
290+
}
291+
}
292+
293+
match UnixListener::bind(path) {
294+
Ok(listener) => Listener::Unix(listener),
295+
Err(e) => {
296+
error!("Failed to bind to Unix socket {}: {}", path, e);
297+
return Err(MagnusError::new(
298+
magnus::exception::runtime_error(),
299+
format!("Failed to bind to Unix socket {}: {}", path, e)
300+
));
301+
}
302+
}
303+
} else {
304+
match config.bind_address.parse::<SocketAddr>() {
305+
Ok(addr) => {
306+
match TcpListener::bind(addr).await {
307+
Ok(listener) => Listener::Tcp(listener),
308+
Err(e) => {
309+
error!("Failed to bind to address {}: {}", addr, e);
310+
return Err(MagnusError::new(
311+
magnus::exception::runtime_error(),
312+
format!("Failed to bind to address {}: {}", addr, e)
313+
));
314+
}
315+
}
316+
},
317+
Err(e) => {
318+
error!("Invalid address format {}: {}", config.bind_address, e);
319+
return Err(MagnusError::new(
320+
magnus::exception::runtime_error(),
321+
format!("Invalid address format {}: {}", config.bind_address, e)
322+
));
323+
}
324+
}
325+
};
279326

327+
// Now that we have successfully bound, spawn the server task
328+
let server_task = tokio::spawn(async move {
280329
let graceful_shutdown = GracefulShutdown::new();
281330
let mut shutdown_rx = shutdown_rx;
282331

test/test_http.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "test_helper"
44
require "httpx"
5+
require "fileutils"
56

67
class TestHttp < HyperRubyTest
78
ACCEPT_RESPONSE = HyperRuby::Response.new(202, { 'Content-Type' => 'text/plain' }, '').freeze
@@ -103,6 +104,117 @@ def test_http2_request
103104
end
104105
end
105106

107+
def test_address_binding_error
108+
# First server binds to the port
109+
server1 = HyperRuby::Server.new
110+
server1.configure({ bind_address: "127.0.0.1:3020" })
111+
server1.start
112+
113+
begin
114+
# Try to start a second server on the same port, which should fail
115+
server2 = HyperRuby::Server.new
116+
server2.configure({ bind_address: "127.0.0.1:3020" })
117+
118+
# This should raise an exception
119+
error = assert_raises(RuntimeError) do
120+
server2.start
121+
end
122+
123+
# Verify that the error message contains information about the binding failure
124+
assert_match(/Failed to bind to address/, error.message)
125+
ensure
126+
# Clean up
127+
server1.stop if server1
128+
end
129+
end
130+
131+
def test_unix_socket_cleanup
132+
# Create a temporary path for the socket
133+
socket_path = "/tmp/hyper_ruby_test_cleanup.sock"
134+
135+
# Clean up any leftover socket file from previous test runs
136+
File.unlink(socket_path) if File.exist?(socket_path)
137+
138+
# First ensure that automatic cleanup works when a socket exists
139+
# but is deletable - this should work without errors
140+
FileUtils.touch(socket_path)
141+
File.chmod(0644, socket_path) # Ensure we have permissions
142+
143+
begin
144+
server = HyperRuby::Server.new
145+
server.configure({ bind_address: "unix:#{socket_path}" })
146+
server.start
147+
148+
# If we get here, the server started correctly
149+
assert File.exist?(socket_path), "Socket file should exist after server starts"
150+
ensure
151+
server.stop if server
152+
File.unlink(socket_path) if File.exist?(socket_path)
153+
end
154+
end
155+
156+
# This test requires root permissions to create a file that can't be deleted.
157+
# Skip it unless we're running with proper permissions.
158+
def test_unix_socket_undeletable
159+
# Skip if we're not root or can't modify file permissions
160+
skip unless Process.uid == 0 || system("sudo -n true 2>/dev/null")
161+
162+
socket_path = "/tmp/hyper_ruby_test_undeletable.sock"
163+
164+
# Clean up any leftover socket file
165+
File.unlink(socket_path) if File.exist?(socket_path)
166+
167+
# Create a file at the socket path that can't be deleted by the current user
168+
system("sudo touch #{socket_path} && sudo chmod 0000 #{socket_path}")
169+
170+
begin
171+
server = HyperRuby::Server.new
172+
server.configure({ bind_address: "unix:#{socket_path}" })
173+
174+
# This should raise an exception about not being able to remove the file
175+
error = assert_raises(RuntimeError) do
176+
server.start
177+
end
178+
179+
# Verify the error message
180+
assert_match(/Failed to remove existing Unix socket file/, error.message)
181+
ensure
182+
# Clean up with sudo
183+
system("sudo rm -f #{socket_path}") if File.exist?(socket_path)
184+
end
185+
end
186+
187+
def test_unix_socket_directory_error
188+
# Create a directory instead of a socket file
189+
socket_dir = "/tmp/hyper_ruby_test_dir"
190+
191+
# Ensure the directory exists
192+
FileUtils.mkdir_p(socket_dir)
193+
194+
# Try to bind to the directory (which should fail)
195+
begin
196+
server = HyperRuby::Server.new
197+
server.configure({ bind_address: "unix:#{socket_dir}" })
198+
199+
# This should raise an exception
200+
error = assert_raises(RuntimeError) do
201+
server.start
202+
end
203+
204+
# The error is from trying to remove the directory, not from binding
205+
assert_match(/Failed to remove existing Unix socket file/, error.message)
206+
207+
# It should include something about "Operation not permitted" or similar
208+
assert(error.message.include?("Operation not permitted") ||
209+
error.message.include?("Permission denied") ||
210+
error.message.include?("not a socket"),
211+
"Error should indicate issue with removing directory: #{error.message}")
212+
ensure
213+
# Clean up
214+
FileUtils.rm_rf(socket_dir) if Dir.exist?(socket_dir)
215+
end
216+
end
217+
106218
private
107219

108220
def handler_simple(request)

0 commit comments

Comments
 (0)