unix_socks 0.2.3 → 0.3.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/CHANGES.md +20 -2
- data/README.md +83 -17
- data/lib/unix_socks/{server.rb → domain_socket_server.rb} +30 -63
- data/lib/unix_socks/server_error.rb +40 -0
- data/lib/unix_socks/server_shared.rb +70 -0
- data/lib/unix_socks/tcp_socket_server.rb +109 -0
- data/lib/unix_socks/version.rb +1 -1
- data/lib/unix_socks.rb +35 -1
- data/spec/unix_socks/{server_spec.rb → domain_socket_server_spec.rb} +47 -15
- data/spec/unix_socks/server_interface_spec.rb +33 -0
- data/spec/unix_socks/tcp_socket_server_spec.rb +137 -0
- data/spec/unix_socks_spec.rb +101 -0
- data/unix_socks.gemspec +5 -5
- metadata +17 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d67e7734f17aa0c0215570741c0d28c24a977e4d5468e35b35b8b51fda5ce3ce
|
|
4
|
+
data.tar.gz: 2aec1808e4c26b8b3948d9aa7d51ed25a641cd72aca74cf4dc611e5a951cf95a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41f9a5ca91d3f57866b204174f1c95c91c56345a040d3cd59513144081a6cfe39124a3dc0e42646b3d9983eff59c139c7d5ec29a89c708b70f6aa688225420f0
|
|
7
|
+
data.tar.gz: 198eccdfe7d548b9c0bbd161afde90e2aa526bdb7540c804998f804177aa212da37429cd1979de56a47ba1e5df178f7e5b79bcf1010048b91965472129cc7519
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2025-12-24 v0.3.0
|
|
4
|
+
|
|
5
|
+
- Refactored socket server architecture by splitting `UnixSocks::Server` into
|
|
6
|
+
`UnixSocks::DomainSocketServer` and `UnixSocks::TCPSocketServer`
|
|
7
|
+
- Added `UnixSocks::ServerShared` module to encapsulate common functionality
|
|
8
|
+
shared between socket server implementations
|
|
9
|
+
- Implemented `to_url` method for both server types to generate `unix://path`
|
|
10
|
+
and `tcp://host:port` URLs
|
|
11
|
+
- Introduced `UnixSocks::ServerError` marker module for consistent error
|
|
12
|
+
handling across the library
|
|
13
|
+
- Updated all documentation and tests to reflect the new dual socket server
|
|
14
|
+
architecture
|
|
15
|
+
- Maintained backward compatibility for core communication patterns
|
|
16
|
+
- Added comprehensive test coverage for both `UnixSocks::DomainSocketServer`
|
|
17
|
+
and `UnixSocks::TCPSocketServer` implementations
|
|
18
|
+
- Added `.rspec` file to configure RSpec with detailed output format (`--format
|
|
19
|
+
d`) for improved test output readability
|
|
20
|
+
|
|
3
21
|
## 2025-12-23 v0.2.3
|
|
4
22
|
|
|
5
23
|
- Handle `Errno::ENOENT` errors in the background socket server thread to
|
|
@@ -7,7 +25,7 @@
|
|
|
7
25
|
- Add test case to verify background thread execution and `ENOENT` error
|
|
8
26
|
handling
|
|
9
27
|
- Maintain existing `at_exit` cleanup behavior to ensure socket file removal
|
|
10
|
-
- Update `UnixSocks::
|
|
28
|
+
- Update `UnixSocks::DomainSocketServer` to be more resilient to temporary file system
|
|
11
29
|
conditions
|
|
12
30
|
- Update `gem_hadar` development dependency to version **2.16.0**
|
|
13
31
|
|
|
@@ -47,7 +65,7 @@
|
|
|
47
65
|
|
|
48
66
|
## 2025-09-07 v0.1.0
|
|
49
67
|
|
|
50
|
-
- Introduced `UnixSocks::
|
|
68
|
+
- Introduced `UnixSocks::DomainSocketServer.default_runtime_dir` class method
|
|
51
69
|
- Simplified coverage configuration by using `GemHadar::SimpleCov.start`
|
|
52
70
|
|
|
53
71
|
## 2025-07-13 v0.0.1
|
data/README.md
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Description
|
|
4
4
|
|
|
5
|
-
A Ruby library for handling Unix
|
|
5
|
+
A Ruby library for handling inter-process communication via Unix sockets and
|
|
6
|
+
TCP sockets.
|
|
6
7
|
|
|
7
8
|
## Features
|
|
8
9
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
10
|
+
- **Dual Socket Support**: Handle both Unix domain sockets and TCP sockets with
|
|
11
|
+
a consistent API
|
|
12
|
+
- **Message Handling**: Simplify sending and receiving messages over sockets
|
|
13
|
+
- **Dynamic Method Access**: Access message body values using method names
|
|
14
|
+
(e.g., `message.key`)
|
|
15
|
+
- **Background Processing**: Run servers in background threads to avoid
|
|
16
|
+
blocking main execution
|
|
17
|
+
- **Robust Error Handling**: Gracefully handle socket disconnections and JSON
|
|
18
|
+
parsing errors
|
|
19
|
+
- **URL Interface**: Servers can be represented as URL strings for easy
|
|
20
|
+
configuration and discovery
|
|
13
21
|
|
|
14
22
|
## Installation
|
|
15
23
|
|
|
@@ -40,10 +48,14 @@ Create a server instance and start listening for connections:
|
|
|
40
48
|
```ruby
|
|
41
49
|
require 'unix_socks'
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
# For Unix sockets
|
|
52
|
+
server = UnixSocks::DomainSocketServer.new(socket_name: 'my_socket')
|
|
53
|
+
|
|
54
|
+
# For TCP sockets
|
|
55
|
+
server = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
44
56
|
|
|
45
57
|
# Run the server in the background to avoid blocking
|
|
46
|
-
thread = server.receive_in_background
|
|
58
|
+
thread = server.receive_in_background do |message|
|
|
47
59
|
puts "Received message: #{message.inspect}"
|
|
48
60
|
end
|
|
49
61
|
|
|
@@ -55,7 +67,11 @@ thread.join
|
|
|
55
67
|
Transmit messages to connected clients:
|
|
56
68
|
|
|
57
69
|
```ruby
|
|
58
|
-
|
|
70
|
+
# For Unix sockets
|
|
71
|
+
client = UnixSocks::DomainSocketServer.new(socket_name: 'my_socket')
|
|
72
|
+
|
|
73
|
+
# For TCP sockets
|
|
74
|
+
client = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
59
75
|
|
|
60
76
|
# Prepare your message
|
|
61
77
|
message = { status: 'success', data: [1, 2, 3] }
|
|
@@ -71,7 +87,11 @@ Handle incoming messages and send responses:
|
|
|
71
87
|
```ruby
|
|
72
88
|
require 'unix_socks'
|
|
73
89
|
|
|
74
|
-
|
|
90
|
+
# For Unix sockets
|
|
91
|
+
server = UnixSocks::DomainSocketServer.new(socket_name: 'my_socket')
|
|
92
|
+
|
|
93
|
+
# For TCP sockets
|
|
94
|
+
server = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
75
95
|
|
|
76
96
|
def handle_message(message)
|
|
77
97
|
# Access message body values using method names
|
|
@@ -82,7 +102,7 @@ def handle_message(message)
|
|
|
82
102
|
end
|
|
83
103
|
|
|
84
104
|
# Use in your server setup
|
|
85
|
-
thread = server.receive_in_background
|
|
105
|
+
thread = server.receive_in_background do |message|
|
|
86
106
|
handle_message(message)
|
|
87
107
|
end
|
|
88
108
|
|
|
@@ -91,23 +111,69 @@ thread.join
|
|
|
91
111
|
|
|
92
112
|
And in the client:
|
|
93
113
|
```ruby
|
|
94
|
-
|
|
114
|
+
# For Unix sockets
|
|
115
|
+
client = UnixSocks::DomainSocketServer.new(socket_name: 'my_socket')
|
|
116
|
+
|
|
117
|
+
# For TCP sockets
|
|
118
|
+
client = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
95
119
|
|
|
96
120
|
# Prepare your message
|
|
97
121
|
message = { status: 'success', data: [1, 2, 3] }
|
|
98
122
|
|
|
99
|
-
# Send the message
|
|
123
|
+
# Send the message and get a response
|
|
100
124
|
response = client.transmit_with_response(message)
|
|
101
125
|
|
|
102
126
|
# Receive the response
|
|
103
127
|
puts "Received server response status: #{response.status}"
|
|
104
128
|
```
|
|
105
129
|
|
|
106
|
-
### 4.
|
|
130
|
+
### 4. Force Parameter Behavior
|
|
131
|
+
|
|
132
|
+
The `force` parameter is only applicable to Unix domain socket servers and
|
|
133
|
+
controls whether existing socket files should be overwritten:
|
|
134
|
+
|
|
135
|
+
- **Unix Socket Servers**: When `force: true` is specified, existing socket
|
|
136
|
+
files will be overwritten without raising an error. Otherwise a
|
|
137
|
+
`UnixSocks::ServerError` is raised.
|
|
138
|
+
- **TCP Socket Servers**: The `force` parameter is accepted for interface
|
|
139
|
+
compatibility but has no effect since TCP sockets don't use filesystem-based
|
|
140
|
+
socket files
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# Unix socket - force parameter works
|
|
144
|
+
server = UnixSocks::DomainSocketServer.new(socket_name: 'my.sock')
|
|
145
|
+
server.receive(force: true) # Overwrites existing socket file if it exists
|
|
146
|
+
|
|
147
|
+
# TCP socket - force parameter is ignored
|
|
148
|
+
server = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
149
|
+
server.receive(force: true) # Parameter has no effect
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5. Server URL Interface
|
|
153
|
+
|
|
154
|
+
Both server types support URL representation for easy configuration and discovery:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Unix socket server URL
|
|
158
|
+
unix_server = UnixSocks::DomainSocketServer.new(socket_name: 'my.sock')
|
|
159
|
+
unix_url = unix_server.to_url # => "unix:///full/path/to/my.sock"
|
|
160
|
+
|
|
161
|
+
# TCP socket server URL
|
|
162
|
+
tcp_server = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
163
|
+
tcp_url = tcp_server.to_url # => "tcp://localhost:8080"
|
|
164
|
+
|
|
165
|
+
# Use URLs for configuration
|
|
166
|
+
server = UnixSocks.from_url(tcp_url)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 6. Message Object Features
|
|
107
170
|
|
|
108
|
-
- **Dynamic Access**: Methods like `message.status` automatically map to the
|
|
109
|
-
|
|
110
|
-
- **
|
|
171
|
+
- **Dynamic Access**: Methods like `message.status` automatically map to the
|
|
172
|
+
message body
|
|
173
|
+
- **Disconnect Handling**: Safely close socket connections using `disconnect`
|
|
174
|
+
- **Error Resilience**: The `respond` method handles disconnections gracefully
|
|
175
|
+
- **Consistent Error Handling**: All server errors are wrapped in
|
|
176
|
+
`UnixSocks::ServerError`
|
|
111
177
|
|
|
112
178
|
## Author
|
|
113
179
|
|
|
@@ -115,4 +181,4 @@ puts "Received server response status: #{response.status}"
|
|
|
115
181
|
|
|
116
182
|
## License
|
|
117
183
|
|
|
118
|
-
[MIT License](
|
|
184
|
+
[MIT License](LICENSE)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Manages Unix socket-based communication, providing both server and client
|
|
2
2
|
# functionality.
|
|
3
|
-
class UnixSocks::
|
|
3
|
+
class UnixSocks::DomainSocketServer
|
|
4
4
|
include FileUtils
|
|
5
|
+
include UnixSocks::ServerShared
|
|
5
6
|
|
|
6
|
-
# Initializes a new UnixSocks::
|
|
7
|
+
# Initializes a new UnixSocks::DomainSocketServer instance.
|
|
7
8
|
#
|
|
8
9
|
# @param socket_name [ String ] The name of the server socket file.
|
|
9
10
|
# @param runtime_dir [ String, nil ] The path to the runtime directory where
|
|
@@ -13,6 +14,17 @@ class UnixSocks::Server
|
|
|
13
14
|
@socket_name, @runtime_dir = socket_name, runtime_dir
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
# Returns the URL representation of the server socket configuration.
|
|
18
|
+
#
|
|
19
|
+
# This method constructs and returns a URL string in the format "unix://path"
|
|
20
|
+
# that represents the Unix socket server's file path
|
|
21
|
+
# configuration.
|
|
22
|
+
#
|
|
23
|
+
# @return [ String ] A URL string in the format "unix://path"
|
|
24
|
+
def to_url
|
|
25
|
+
"unix://#{server_socket_path}"
|
|
26
|
+
end
|
|
27
|
+
|
|
16
28
|
# Returns the default runtime directory path based on the XDG_RUNTIME_DIR
|
|
17
29
|
# environment variable.
|
|
18
30
|
#
|
|
@@ -47,29 +59,23 @@ class UnixSocks::Server
|
|
|
47
59
|
File.expand_path(File.join(@runtime_dir, @socket_name))
|
|
48
60
|
end
|
|
49
61
|
|
|
50
|
-
# The transmit method sends a message over the Unix socket connection
|
|
62
|
+
# The transmit method sends a message over the Unix socket connection
|
|
51
63
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
64
|
+
# This method prepares a message by converting it to JSON format, establishes
|
|
65
|
+
# a connection to the server socket using UNIXSocket.new, writes the
|
|
66
|
+
# prepared message to the socket, and then returns the created socket
|
|
54
67
|
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
68
|
+
# @param message [ Object ] The message to be sent over the Unix socket
|
|
69
|
+
# @param close [ TrueClass, FalseClass ] Whether to close the socket after sending
|
|
57
70
|
#
|
|
58
|
-
# @
|
|
59
|
-
def transmit(message)
|
|
71
|
+
# @return [ UNIXSocket ] The socket connection that was used to transmit the message
|
|
72
|
+
def transmit(message, close: false)
|
|
60
73
|
mkdir_p @runtime_dir
|
|
61
74
|
socket = UNIXSocket.new(server_socket_path)
|
|
62
75
|
socket.puts JSON(message)
|
|
63
76
|
socket
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Sends a message and returns the parsed JSON response.
|
|
67
|
-
#
|
|
68
|
-
# @param message [Object] The message to be sent as JSON.
|
|
69
|
-
# @return [Hash, nil] The parsed JSON response or nil if parsing fails.
|
|
70
|
-
def transmit_with_response(message)
|
|
71
|
-
socket = transmit(message)
|
|
72
|
-
parse_json_message(socket.gets, socket)
|
|
77
|
+
ensure
|
|
78
|
+
close and socket.close
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
# Receives messages from clients connected to the server socket.
|
|
@@ -87,7 +93,9 @@ class UnixSocks::Server
|
|
|
87
93
|
def receive(force: false, &block)
|
|
88
94
|
mkdir_p @runtime_dir
|
|
89
95
|
if !force && socket_path_exist?
|
|
90
|
-
raise
|
|
96
|
+
raise UnixSocks::ServerError.build(
|
|
97
|
+
Errno::EEXIST, "Path already exists #{server_socket_path.inspect}"
|
|
98
|
+
)
|
|
91
99
|
end
|
|
92
100
|
Socket.unix_server_loop(server_socket_path) do |socket, client_addrinfo|
|
|
93
101
|
message = pop_message(socket) and block.(message)
|
|
@@ -107,7 +115,9 @@ class UnixSocks::Server
|
|
|
107
115
|
# @return [Thread] The background thread running the receiver
|
|
108
116
|
def receive_in_background(force: false, &block)
|
|
109
117
|
if !force && socket_path_exist?
|
|
110
|
-
raise
|
|
118
|
+
raise UnixSocks::ServerError.build(
|
|
119
|
+
Errno::EEXIST, "Path already exists #{server_socket_path.inspect}"
|
|
120
|
+
)
|
|
111
121
|
end
|
|
112
122
|
Thread.new do
|
|
113
123
|
receive(force:, &block)
|
|
@@ -128,47 +138,4 @@ class UnixSocks::Server
|
|
|
128
138
|
def remove_socket_path
|
|
129
139
|
FileUtils.rm_f server_socket_path
|
|
130
140
|
end
|
|
131
|
-
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
# Parses a JSON message from the socket and associates it with the socket
|
|
135
|
-
# connection
|
|
136
|
-
#
|
|
137
|
-
# This method retrieves a line of data from the socket, strips whitespace,
|
|
138
|
-
# and attempts to parse it as JSON. If successful, it creates a
|
|
139
|
-
# UnixSocks::Message object with the parsed data and assigns the socket
|
|
140
|
-
# connection to the message. If parsing fails, it logs a warning and
|
|
141
|
-
# returns nil.
|
|
142
|
-
#
|
|
143
|
-
# @param socket [ Socket ] the socket connection to read from
|
|
144
|
-
#
|
|
145
|
-
# @return [ UnixSocks::Message, nil ] the parsed message object or nil if
|
|
146
|
-
# parsing fails
|
|
147
|
-
def pop_message(socket)
|
|
148
|
-
parse_json_message(socket.gets, socket)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Parses a JSON message from the given data and associates it with the
|
|
152
|
-
# provided socket connection.
|
|
153
|
-
#
|
|
154
|
-
# This method processes the input data by stripping whitespace and attempting
|
|
155
|
-
# to parse it as JSON. If parsing is successful, it creates a
|
|
156
|
-
# UnixSocks::Message object with the parsed data and assigns the socket
|
|
157
|
-
# connection to the message. In case of a JSON parsing error, it
|
|
158
|
-
# logs a warning and returns nil.
|
|
159
|
-
#
|
|
160
|
-
# @param data [ String ] The raw data string to be parsed as JSON.
|
|
161
|
-
# @param socket [ Socket ] The socket connection associated with the message.
|
|
162
|
-
#
|
|
163
|
-
# @return [ UnixSocks::Message, nil ] The parsed message object or nil if
|
|
164
|
-
# parsing fails.
|
|
165
|
-
def parse_json_message(data, socket)
|
|
166
|
-
data = data.strip
|
|
167
|
-
data.empty? and return nil
|
|
168
|
-
obj = JSON.parse(data, object_class: UnixSocks::Message)
|
|
169
|
-
obj.socket = socket
|
|
170
|
-
obj
|
|
171
|
-
rescue JSON::ParserError => e
|
|
172
|
-
warn "Caught #{e.class}: #{e} for #{data[0, 512].inspect}"
|
|
173
|
-
end
|
|
174
141
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Marker module for UnixSocks server-related errors.
|
|
2
|
+
#
|
|
3
|
+
# This module is mixed into exceptions raised by UnixSocks server
|
|
4
|
+
# implementations to provide a common rescue clause for server-related
|
|
5
|
+
# errors.
|
|
6
|
+
#
|
|
7
|
+
# @example Handling server errors
|
|
8
|
+
# begin
|
|
9
|
+
# server.receive { |message| process_message(message) }
|
|
10
|
+
# rescue UnixSocks::ServerError
|
|
11
|
+
# # Handle all UnixSocks server errors consistently
|
|
12
|
+
# end
|
|
13
|
+
module UnixSocks::ServerError
|
|
14
|
+
# Builds a server error exception with the given exception class and message.
|
|
15
|
+
#
|
|
16
|
+
# This method creates a new exception instance of the specified exception
|
|
17
|
+
# class with the provided message, marks it with the ServerError module, and
|
|
18
|
+
# returns the marked exception.
|
|
19
|
+
#
|
|
20
|
+
# @param exception [ Class ] The exception class to be instantiated
|
|
21
|
+
# @param message [ String ] The error message for the exception
|
|
22
|
+
#
|
|
23
|
+
# @return [ Exception ] A new exception instance marked with ServerError module
|
|
24
|
+
def self.build(exception, message)
|
|
25
|
+
mark(exception.new(message))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Marks the given exception with the ServerError module.
|
|
29
|
+
#
|
|
30
|
+
# This method extends the provided exception object with the ServerError
|
|
31
|
+
# module, effectively tagging it as a server-related error for consistent
|
|
32
|
+
# handling.
|
|
33
|
+
#
|
|
34
|
+
# @param e [ Exception ] The exception object to be marked
|
|
35
|
+
#
|
|
36
|
+
# @return [ Exception ] The same exception object, now extended with ServerError
|
|
37
|
+
def self.mark(e)
|
|
38
|
+
e.extend(self)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Shared server functionality for UnixSocks implementations
|
|
2
|
+
#
|
|
3
|
+
# This module provides common methods for transmitting messages and parsing
|
|
4
|
+
# JSON responses that are used by both Unix domain socket and TCP socket
|
|
5
|
+
# server implementations.
|
|
6
|
+
module UnixSocks::ServerShared
|
|
7
|
+
# Sends a message and returns the parsed JSON response.
|
|
8
|
+
#
|
|
9
|
+
# @param message [Object] The message to be sent as JSON.
|
|
10
|
+
# @return [Hash, nil] The parsed JSON response or nil if parsing fails.
|
|
11
|
+
def transmit_with_response(message, close: true)
|
|
12
|
+
socket = transmit(message, close: false)
|
|
13
|
+
parse_json_message(socket.gets, socket)
|
|
14
|
+
ensure
|
|
15
|
+
close and socket.close
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Converts the server's URL representation into a URI object.
|
|
19
|
+
#
|
|
20
|
+
# This method takes the URL string returned by #to_url and parses it into
|
|
21
|
+
# a URI object for convenient access to the server's address components.
|
|
22
|
+
#
|
|
23
|
+
# @return [ URI ] A URI object representing the server's address configuration.
|
|
24
|
+
def to_uri
|
|
25
|
+
URI.parse(to_url)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Parses a JSON message from the socket and associates it with the socket
|
|
31
|
+
# connection
|
|
32
|
+
#
|
|
33
|
+
# This method retrieves a line of data from the socket, strips whitespace,
|
|
34
|
+
# and attempts to parse it as JSON. If successful, it creates a
|
|
35
|
+
# UnixSocks::Message object with the parsed data and assigns the socket
|
|
36
|
+
# connection to the message. If parsing fails, it logs a warning and
|
|
37
|
+
# returns nil.
|
|
38
|
+
#
|
|
39
|
+
# @param socket [ Socket ] the socket connection to read from
|
|
40
|
+
#
|
|
41
|
+
# @return [ UnixSocks::Message, nil ] the parsed message object or nil if
|
|
42
|
+
# parsing fails
|
|
43
|
+
def pop_message(socket)
|
|
44
|
+
parse_json_message(socket.gets, socket)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Parses a JSON message from the given data and associates it with the
|
|
48
|
+
# provided socket connection.
|
|
49
|
+
#
|
|
50
|
+
# This method processes the input data by stripping whitespace and attempting
|
|
51
|
+
# to parse it as JSON. If parsing is successful, it creates a
|
|
52
|
+
# UnixSocks::Message object with the parsed data and assigns the socket
|
|
53
|
+
# connection to the message. In case of a JSON parsing error, it
|
|
54
|
+
# logs a warning and returns nil.
|
|
55
|
+
#
|
|
56
|
+
# @param data [ String ] The raw data string to be parsed as JSON.
|
|
57
|
+
# @param socket [ Socket ] The socket connection associated with the message.
|
|
58
|
+
#
|
|
59
|
+
# @return [ UnixSocks::Message, nil ] The parsed message object or nil if
|
|
60
|
+
# parsing fails.
|
|
61
|
+
def parse_json_message(data, socket)
|
|
62
|
+
data = data.strip
|
|
63
|
+
data.empty? and return nil
|
|
64
|
+
obj = JSON.parse(data, object_class: UnixSocks::Message)
|
|
65
|
+
obj.socket = socket
|
|
66
|
+
obj
|
|
67
|
+
rescue JSON::ParserError => e
|
|
68
|
+
warn "Caught #{e.class}: #{e} for #{data[0, 512].inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Provides TCP socket-based communication for inter-process messaging.
|
|
2
|
+
#
|
|
3
|
+
# This class enables sending and receiving messages over TCP connections,
|
|
4
|
+
# supporting both client and server functionality for network-based
|
|
5
|
+
# communication.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# server = UnixSocks::TCPSocketServer.new(hostname: 'localhost', port: 8080)
|
|
9
|
+
# server.receive { |message| puts message.inspect }
|
|
10
|
+
# server.transmit({ message: 'hello' })
|
|
11
|
+
class UnixSocks::TCPSocketServer
|
|
12
|
+
include UnixSocks::ServerShared
|
|
13
|
+
|
|
14
|
+
# Initializes a new UnixSocks::TCPSocketServer instance.
|
|
15
|
+
#
|
|
16
|
+
# Sets up the server with the specified hostname and port for TCP
|
|
17
|
+
# communication.
|
|
18
|
+
#
|
|
19
|
+
# @param hostname [ String ] The hostname to bind to, defaults to 'localhost'
|
|
20
|
+
# @param port [ Integer ] The port number to bind to
|
|
21
|
+
def initialize(hostname: 'localhost', port:)
|
|
22
|
+
@hostname, @port = hostname, port
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns the hostname associated with this TCP socket server instance.
|
|
26
|
+
#
|
|
27
|
+
# @return [ String ] The hostname that the server binds to for TCP communication.
|
|
28
|
+
attr_reader :hostname
|
|
29
|
+
|
|
30
|
+
# Returns the port number associated with this TCP socket server instance.
|
|
31
|
+
#
|
|
32
|
+
# @return [ Integer ] The port number that the server binds to for TCP communication.
|
|
33
|
+
attr_reader :port
|
|
34
|
+
|
|
35
|
+
# Returns the URL representation of the TCP socket server configuration.
|
|
36
|
+
#
|
|
37
|
+
# This method constructs and returns a URL string in the format
|
|
38
|
+
# "tcp://hostname:port" that represents the TCP socket server's address and
|
|
39
|
+
# port configuration.
|
|
40
|
+
#
|
|
41
|
+
# @return [ String ] A URL string in the format "tcp://hostname:port"
|
|
42
|
+
def to_url
|
|
43
|
+
"tcp://#@hostname:#@port"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Sends a message over a TCP connection to the configured hostname and port.
|
|
47
|
+
#
|
|
48
|
+
# This method establishes a TCP socket connection to the specified hostname
|
|
49
|
+
# and port, serializes the provided message to JSON format, and writes it to
|
|
50
|
+
# the socket. The socket connection is returned after the message is sent.
|
|
51
|
+
#
|
|
52
|
+
# @param message [Object] The message to be sent, which will be converted to JSON
|
|
53
|
+
# @param close [TrueClass, FalseClass] Whether to close the socket after sending
|
|
54
|
+
#
|
|
55
|
+
# @return [TCPSocket] The socket connection that was used to transmit the message
|
|
56
|
+
def transmit(message, close: false)
|
|
57
|
+
socket = TCPSocket.new(@hostname, @port)
|
|
58
|
+
socket.puts JSON(message)
|
|
59
|
+
socket
|
|
60
|
+
ensure
|
|
61
|
+
close and socket.close
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Receives messages from clients connected to the TCP socket.
|
|
65
|
+
#
|
|
66
|
+
# This method binds to the configured hostname and port, listens for incoming
|
|
67
|
+
# connections, and processes messages from connected clients. It accepts a
|
|
68
|
+
# single connection at a time and handles each message by parsing it from
|
|
69
|
+
# JSON and yielding it to the provided block. The socket connection is closed
|
|
70
|
+
# after processing each message.
|
|
71
|
+
#
|
|
72
|
+
# @param force [ nil ] This parameter is accepted for interface compatibility
|
|
73
|
+
# but is unused in the TCP implementation.
|
|
74
|
+
# @yield [ UnixSocks::Message ] The received message parsed from JSON.
|
|
75
|
+
#
|
|
76
|
+
# @raise [ Errno::EADDRINUSE ] If the address is already in use.
|
|
77
|
+
def receive(force: nil, &block)
|
|
78
|
+
Addrinfo.tcp(@hostname, @port).bind do |server|
|
|
79
|
+
server.listen(1)
|
|
80
|
+
loop do
|
|
81
|
+
socket, = server.accept
|
|
82
|
+
message = pop_message(socket) and block.(message)
|
|
83
|
+
socket.close
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
rescue Errno::EADDRINUSE => e
|
|
87
|
+
raise UnixSocks::ServerError.mark(e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Runs the message receiver in a background thread to prevent blocking.
|
|
91
|
+
#
|
|
92
|
+
# This method starts a new thread that continuously listens for incoming
|
|
93
|
+
# messages from connected clients. The server socket is created in the
|
|
94
|
+
# background, allowing the main execution flow to continue without
|
|
95
|
+
# waiting for messages.
|
|
96
|
+
#
|
|
97
|
+
# @param force [ nil ] This parameter is accepted for interface compatibility
|
|
98
|
+
# but is unused in the TCP implementation.
|
|
99
|
+
# @yield [UnixSocks::Message] The received message
|
|
100
|
+
#
|
|
101
|
+
# @return [Thread] The background thread running the receiver
|
|
102
|
+
def receive_in_background(force: nil, &block)
|
|
103
|
+
Thread.new do
|
|
104
|
+
receive(&block)
|
|
105
|
+
rescue Errno::EADDRINUSE => e
|
|
106
|
+
raise UnixSocks::ServerError.mark(e)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/unix_socks/version.rb
CHANGED
data/lib/unix_socks.rb
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'fileutils'
|
|
3
3
|
require 'socket'
|
|
4
|
+
require 'uri'
|
|
4
5
|
require 'tins'
|
|
5
6
|
|
|
6
7
|
# Provides classes for handling inter-process communication via Unix 🧦🧦.
|
|
7
8
|
# Supports dynamic message handling, background processing, and robust error
|
|
8
9
|
# management.
|
|
9
10
|
module UnixSocks
|
|
11
|
+
# Creates a server instance from a URL string.
|
|
12
|
+
#
|
|
13
|
+
# This method parses a URL and constructs the appropriate server instance
|
|
14
|
+
# based on the scheme. For 'unix' URLs, it creates a DomainSocketServer,
|
|
15
|
+
# and for 'tcp' URLs, it creates a TCPSocketServer.
|
|
16
|
+
#
|
|
17
|
+
# @param url [String, URI] The URL string or URI object representing the
|
|
18
|
+
# server configuration
|
|
19
|
+
#
|
|
20
|
+
# @return [UnixSocks::DomainSocketServer, UnixSocks::TCPSocketServer] The
|
|
21
|
+
# constructed server instance
|
|
22
|
+
#
|
|
23
|
+
# @raise [ArgumentError] If the URL scheme is not 'unix' or 'tcp'
|
|
24
|
+
def self.from_url(url)
|
|
25
|
+
uri = url.is_a?(URI) ? url : URI.parse(url.to_s)
|
|
26
|
+
case uri.scheme
|
|
27
|
+
when 'unix'
|
|
28
|
+
DomainSocketServer.new(
|
|
29
|
+
socket_name: File.basename(uri.path),
|
|
30
|
+
runtime_dir: File.dirname(uri.path)
|
|
31
|
+
)
|
|
32
|
+
when 'tcp'
|
|
33
|
+
TCPSocketServer.new(
|
|
34
|
+
hostname: uri.host,
|
|
35
|
+
port: uri.port
|
|
36
|
+
)
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, "Invalid URL #{url.to_s.inspect} for UnixSocks"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
10
41
|
end
|
|
11
42
|
|
|
12
43
|
require 'unix_socks/version'
|
|
44
|
+
require 'unix_socks/server_error'
|
|
13
45
|
require 'unix_socks/message'
|
|
14
|
-
require 'unix_socks/
|
|
46
|
+
require 'unix_socks/server_shared'
|
|
47
|
+
require 'unix_socks/domain_socket_server'
|
|
48
|
+
require 'unix_socks/tcp_socket_server'
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
require 'tins/xt/expose'
|
|
3
3
|
|
|
4
|
-
describe UnixSocks::
|
|
4
|
+
describe UnixSocks::DomainSocketServer do
|
|
5
5
|
let(:socket_name) { 'test_socket' }
|
|
6
6
|
let(:runtime_dir) { './tmp' }
|
|
7
|
-
let(:server) {
|
|
7
|
+
let(:server) {
|
|
8
|
+
described_class.new(socket_name: socket_name, runtime_dir: runtime_dir).expose
|
|
9
|
+
}
|
|
8
10
|
|
|
9
11
|
describe '#initialize' do
|
|
10
12
|
it 'sets the socket name and runtime directory' do
|
|
@@ -46,9 +48,18 @@ describe UnixSocks::Server do
|
|
|
46
48
|
it 'sends a message over the Unix socket' do
|
|
47
49
|
expect(server).to receive(:mkdir_p).with(runtime_dir)
|
|
48
50
|
expect(UNIXSocket).to receive(:new).with(server.server_socket_path).
|
|
49
|
-
and_return(double('socket', puts: nil
|
|
51
|
+
and_return(double('socket', puts: nil))
|
|
50
52
|
server.transmit(message)
|
|
51
53
|
end
|
|
54
|
+
|
|
55
|
+
it 'sends a message over the Unix socket and close' do
|
|
56
|
+
expect(server).to receive(:mkdir_p).with(runtime_dir)
|
|
57
|
+
socket = double('socket', puts: nil)
|
|
58
|
+
expect(UNIXSocket).to receive(:new).with(server.server_socket_path).
|
|
59
|
+
and_return(socket)
|
|
60
|
+
expect(socket).to receive(:close)
|
|
61
|
+
server.transmit(message, close: true)
|
|
62
|
+
end
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
describe '#transmit_with_response' do
|
|
@@ -56,7 +67,12 @@ describe UnixSocks::Server do
|
|
|
56
67
|
|
|
57
68
|
it 'parses a valid JSON response' do
|
|
58
69
|
allow(server).to receive(:mkdir_p)
|
|
59
|
-
mock_socket = double(
|
|
70
|
+
mock_socket = double(
|
|
71
|
+
'socket',
|
|
72
|
+
puts: nil,
|
|
73
|
+
gets: '{"status": "success"}',
|
|
74
|
+
close: true
|
|
75
|
+
)
|
|
60
76
|
expect(UNIXSocket).to receive(:new).and_return(mock_socket)
|
|
61
77
|
|
|
62
78
|
response = server.transmit_with_response(message)
|
|
@@ -66,7 +82,12 @@ describe UnixSocks::Server do
|
|
|
66
82
|
|
|
67
83
|
it 'handles JSON parsing errors' do
|
|
68
84
|
allow(server).to receive(:mkdir_p)
|
|
69
|
-
mock_socket = double(
|
|
85
|
+
mock_socket = double(
|
|
86
|
+
'socket',
|
|
87
|
+
puts: nil,
|
|
88
|
+
gets: 'invalid_json',
|
|
89
|
+
close: true
|
|
90
|
+
)
|
|
70
91
|
expect(server).to receive(:warn).
|
|
71
92
|
with(/Caught JSON::ParserError: unexpected character: 'invalid_json'/)
|
|
72
93
|
expect(UNIXSocket).to receive(:new).and_return(mock_socket)
|
|
@@ -77,7 +98,12 @@ describe UnixSocks::Server do
|
|
|
77
98
|
|
|
78
99
|
it 'handles empty responses' do
|
|
79
100
|
allow(server).to receive(:mkdir_p)
|
|
80
|
-
mock_socket = double(
|
|
101
|
+
mock_socket = double(
|
|
102
|
+
'socket',
|
|
103
|
+
puts: nil,
|
|
104
|
+
gets: '',
|
|
105
|
+
close: true
|
|
106
|
+
)
|
|
81
107
|
expect(UNIXSocket).to receive(:new).and_return(mock_socket)
|
|
82
108
|
|
|
83
109
|
response = server.transmit_with_response(message)
|
|
@@ -89,7 +115,7 @@ describe UnixSocks::Server do
|
|
|
89
115
|
it 'raises an error if the socket already exists and force is false' do
|
|
90
116
|
allow(server).to receive(:socket_path_exist?).and_return(true)
|
|
91
117
|
expect { server.receive(force: false) }.to\
|
|
92
|
-
raise_error(
|
|
118
|
+
raise_error(UnixSocks::ServerError, /Path already exists/)
|
|
93
119
|
end
|
|
94
120
|
|
|
95
121
|
it 'does not raise an error if force is true' do
|
|
@@ -120,15 +146,15 @@ describe UnixSocks::Server do
|
|
|
120
146
|
|
|
121
147
|
describe '#receive_in_background' do
|
|
122
148
|
it 'runs the receiver in a background thread' do
|
|
123
|
-
expect(Thread).to receive(:new).and_yield
|
|
149
|
+
expect(Thread).to receive(:new).and_yield.and_return(double(join: true))
|
|
124
150
|
expect(FileUtils).to receive(:rm_f).with(server.server_socket_path)
|
|
125
151
|
expect(server).to receive(:at_exit) { |&block| block.call }
|
|
126
152
|
expect(server).to receive(:receive).with(force: true)
|
|
127
153
|
|
|
128
|
-
server.receive_in_background(force: true)
|
|
154
|
+
server.receive_in_background(force: true).join
|
|
129
155
|
end
|
|
130
156
|
|
|
131
|
-
it 'it raises
|
|
157
|
+
it 'it raises UnixSocks::ServerError if socket already exists' do
|
|
132
158
|
expect(Thread).not_to receive(:new).and_yield
|
|
133
159
|
expect(FileUtils).not_to receive(:rm_f).with(server.server_socket_path)
|
|
134
160
|
expect(server).to receive(:socket_path_exist?).and_return true
|
|
@@ -136,22 +162,28 @@ describe UnixSocks::Server do
|
|
|
136
162
|
expect(server).not_to receive(:receive).with(force: false)
|
|
137
163
|
|
|
138
164
|
expect {
|
|
139
|
-
server.receive_in_background(force: false)
|
|
140
|
-
}.to raise_error(
|
|
165
|
+
server.receive_in_background(force: false).join
|
|
166
|
+
}.to raise_error(UnixSocks::ServerError)
|
|
141
167
|
end
|
|
142
168
|
|
|
143
|
-
it 'runs the receiver in a background thread' do
|
|
144
|
-
expect(Thread).to receive(:new).and_yield
|
|
169
|
+
it 'runs the receiver in a background thread ignoring existing sockets' do
|
|
170
|
+
expect(Thread).to receive(:new).and_yield.and_return(double(join: true))
|
|
145
171
|
expect(FileUtils).to receive(:rm_f).with(server.server_socket_path)
|
|
146
172
|
expect(server).to receive(:at_exit) { |&block| block.call }
|
|
147
173
|
expect(server).to receive(:receive).and_raise Errno::ENOENT
|
|
148
174
|
|
|
149
175
|
expect {
|
|
150
|
-
server.receive_in_background(force: true)
|
|
176
|
+
server.receive_in_background(force: true).join
|
|
151
177
|
}.not_to raise_error
|
|
152
178
|
end
|
|
153
179
|
end
|
|
154
180
|
|
|
181
|
+
describe '#to_uri' do
|
|
182
|
+
it 'displays address' do
|
|
183
|
+
expect(server.to_url).to match %r(\Aunix://.*test_socket\z)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
155
187
|
describe '#socket_path_exist?' do
|
|
156
188
|
it 'returns false if the socket file does not exist' do
|
|
157
189
|
FileUtils.rm_f(server.server_socket_path)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'UnixSocks::Server Interface' do
|
|
4
|
+
shared_examples 'sufficient interface' do
|
|
5
|
+
it 'supports transmit' do
|
|
6
|
+
expect(described_class).to be_method_defined :transmit
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'supports transmit_with_response' do
|
|
10
|
+
expect(described_class).to be_method_defined :transmit_with_response
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'supports receive' do
|
|
14
|
+
expect(described_class).to be_method_defined :receive
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'supports receive_in_background' do
|
|
18
|
+
expect(described_class).to be_method_defined :receive_in_background
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'supports to_uri' do
|
|
22
|
+
expect(described_class).to be_method_defined :to_url
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context UnixSocks::DomainSocketServer do
|
|
27
|
+
it_behaves_like 'sufficient interface'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context UnixSocks::TCPSocketServer do
|
|
31
|
+
it_behaves_like 'sufficient interface'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tins/xt/expose'
|
|
3
|
+
|
|
4
|
+
describe UnixSocks::TCPSocketServer do
|
|
5
|
+
let(:server) { described_class.new(hostname: 'localhost', port: 1234).expose }
|
|
6
|
+
|
|
7
|
+
describe '#initialize' do
|
|
8
|
+
it 'sets the socket name and runtime directory' do
|
|
9
|
+
expect(server.instance_variable_get(:@hostname)).to eq('localhost')
|
|
10
|
+
expect(server.instance_variable_get(:@port)).to eq(1234)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#transmit' do
|
|
15
|
+
let(:message) { { test: 'message' } }
|
|
16
|
+
|
|
17
|
+
it 'sends a message over the Unix socket' do
|
|
18
|
+
expect(TCPSocket).to receive(:new).with(server.hostname, server.port).
|
|
19
|
+
and_return(double('socket', puts: nil))
|
|
20
|
+
server.transmit(message)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'sends a message over the Unix socket and close' do
|
|
24
|
+
socket = double('socket', puts: nil)
|
|
25
|
+
expect(TCPSocket).to receive(:new).with(server.hostname, server.port).
|
|
26
|
+
and_return(socket)
|
|
27
|
+
expect(socket).to receive(:close)
|
|
28
|
+
server.transmit(message, close: true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#transmit_with_response' do
|
|
33
|
+
let(:message) { { test: 'message' } }
|
|
34
|
+
|
|
35
|
+
it 'parses a valid JSON response' do
|
|
36
|
+
allow(server).to receive(:mkdir_p)
|
|
37
|
+
socket = double(
|
|
38
|
+
'socket',
|
|
39
|
+
puts: nil,
|
|
40
|
+
gets: '{"status": "success"}',
|
|
41
|
+
close: true
|
|
42
|
+
)
|
|
43
|
+
expect(TCPSocket).to receive(:new).and_return(socket)
|
|
44
|
+
|
|
45
|
+
response = server.transmit_with_response(message)
|
|
46
|
+
expect(response).to be_a UnixSocks::Message
|
|
47
|
+
expect(response.status).to eq 'success'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'handles JSON parsing errors' do
|
|
51
|
+
allow(server).to receive(:mkdir_p)
|
|
52
|
+
socket = double(
|
|
53
|
+
'socket',
|
|
54
|
+
puts: nil,
|
|
55
|
+
gets: 'invalid_json',
|
|
56
|
+
close: true
|
|
57
|
+
)
|
|
58
|
+
expect(server).to receive(:warn).
|
|
59
|
+
with(/Caught JSON::ParserError: unexpected character: 'invalid_json'/)
|
|
60
|
+
expect(TCPSocket).to receive(:new).and_return(socket)
|
|
61
|
+
|
|
62
|
+
response = server.transmit_with_response(message)
|
|
63
|
+
expect(response).to be nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'handles empty responses' do
|
|
67
|
+
allow(server).to receive(:mkdir_p)
|
|
68
|
+
socket = double(
|
|
69
|
+
'socket',
|
|
70
|
+
puts: nil,
|
|
71
|
+
gets: '',
|
|
72
|
+
close: true
|
|
73
|
+
)
|
|
74
|
+
expect(TCPSocket).to receive(:new).and_return(socket)
|
|
75
|
+
|
|
76
|
+
response = server.transmit_with_response(message)
|
|
77
|
+
expect(response).to be nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe '#receive' do
|
|
82
|
+
it 'does bind to hostname:port' do
|
|
83
|
+
expect(Addrinfo).to receive(:tcp).with(server.hostname, server.port).
|
|
84
|
+
and_return(double(bind: true))
|
|
85
|
+
server.receive
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'raises UnixSocks::ServerError if already bound' do
|
|
89
|
+
socket = double('socket')
|
|
90
|
+
expect(Addrinfo).to receive(:tcp).with(server.hostname, server.port).
|
|
91
|
+
and_return(socket)
|
|
92
|
+
expect(socket).to receive(:bind).and_raise Errno::EADDRINUSE
|
|
93
|
+
expect { server.receive }.to raise_error(UnixSocks::ServerError)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe 'pop_message' do
|
|
98
|
+
it 'parses a valid JSON message' do
|
|
99
|
+
socket = double('socket')
|
|
100
|
+
allow(socket).to receive(:gets).and_return('{"test": "message"}')
|
|
101
|
+
|
|
102
|
+
message = server.pop_message(socket)
|
|
103
|
+
expect(message).to be_a(UnixSocks::Message)
|
|
104
|
+
expect(message.test).to eq 'message'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'handles a JSON parsing error' do
|
|
108
|
+
socket = double('socket')
|
|
109
|
+
allow(socket).to receive(:gets).and_return('invalid_json')
|
|
110
|
+
expect(server).to receive(:warn).
|
|
111
|
+
with(/Caught JSON::ParserError: unexpected character: 'invalid_json'/)
|
|
112
|
+
expect(server.pop_message(socket)).to be nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe '#receive_in_background' do
|
|
117
|
+
it 'runs the receiver in a background thread' do
|
|
118
|
+
expect(Thread).to receive(:new).and_yield.and_return(double(join: true))
|
|
119
|
+
expect(server).to receive(:receive)
|
|
120
|
+
|
|
121
|
+
server.receive_in_background.join
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'it raises UnixSocks::ServerError if socket already exists' do
|
|
125
|
+
expect(server).to receive(:receive).and_raise(Errno::EADDRINUSE)
|
|
126
|
+
expect {
|
|
127
|
+
server.receive_in_background.join
|
|
128
|
+
}.to raise_error(UnixSocks::ServerError)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe '#to_uri' do
|
|
133
|
+
it 'displays address' do
|
|
134
|
+
expect(server.to_url).to eq 'tcp://localhost:1234'
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe UnixSocks do
|
|
4
|
+
describe '.from_url' do
|
|
5
|
+
context 'with unix URL' do
|
|
6
|
+
it 'creates a DomainSocketServer' do
|
|
7
|
+
server = UnixSocks.from_url('unix:///tmp/my.sock')
|
|
8
|
+
expect(server).to be_a(UnixSocks::DomainSocketServer)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'sets correct socket name and runtime directory' do
|
|
12
|
+
server = UnixSocks.from_url('unix:///tmp/my.sock')
|
|
13
|
+
expect(server.instance_variable_get(:@socket_name)).to eq('my.sock')
|
|
14
|
+
expect(server.instance_variable_get(:@runtime_dir)).to eq('/tmp')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'handles unix URL with different runtime directory' do
|
|
18
|
+
server = UnixSocks.from_url('unix:///var/run/my.sock')
|
|
19
|
+
expect(server.instance_variable_get(:@socket_name)).to eq('my.sock')
|
|
20
|
+
expect(server.instance_variable_get(:@runtime_dir)).to eq('/var/run')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'works with URI objects' do
|
|
24
|
+
uri = URI.parse('unix:///tmp/test.sock')
|
|
25
|
+
server = UnixSocks.from_url(uri)
|
|
26
|
+
expect(server).to be_a(UnixSocks::DomainSocketServer)
|
|
27
|
+
expect(server.instance_variable_get(:@socket_name)).to eq('test.sock')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'with tcp URL' do
|
|
32
|
+
it 'creates a TCPSocketServer' do
|
|
33
|
+
server = UnixSocks.from_url('tcp://localhost:8080')
|
|
34
|
+
expect(server).to be_a(UnixSocks::TCPSocketServer)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'sets correct hostname and port' do
|
|
38
|
+
server = UnixSocks.from_url('tcp://example.com:9000')
|
|
39
|
+
expect(server.instance_variable_get(:@hostname)).to eq('example.com')
|
|
40
|
+
expect(server.instance_variable_get(:@port)).to eq(9000)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'works with default port' do
|
|
44
|
+
server = UnixSocks.from_url('tcp://localhost:80')
|
|
45
|
+
expect(server.instance_variable_get(:@hostname)).to eq('localhost')
|
|
46
|
+
expect(server.instance_variable_get(:@port)).to eq(80)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'works with URI objects' do
|
|
50
|
+
uri = URI.parse('tcp://localhost:8080')
|
|
51
|
+
server = UnixSocks.from_url(uri)
|
|
52
|
+
expect(server).to be_a(UnixSocks::TCPSocketServer)
|
|
53
|
+
expect(server.instance_variable_get(:@hostname)).to eq('localhost')
|
|
54
|
+
expect(server.instance_variable_get(:@port)).to eq(8080)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context 'with invalid URL' do
|
|
59
|
+
it 'raises ArgumentError for unsupported scheme' do
|
|
60
|
+
expect {
|
|
61
|
+
UnixSocks.from_url('ftp://example.com/file')
|
|
62
|
+
}.to raise_error(ArgumentError, /Invalid URL.*ftp/)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'raises ArgumentError for invalid URL format' do
|
|
66
|
+
expect {
|
|
67
|
+
UnixSocks.from_url('not_a_url')
|
|
68
|
+
}.to raise_error(ArgumentError)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'with edge cases' do
|
|
73
|
+
it 'handles URLs with query parameters' do
|
|
74
|
+
server = UnixSocks.from_url('unix:///tmp/test.sock?param=value')
|
|
75
|
+
expect(server).to be_a(UnixSocks::DomainSocketServer)
|
|
76
|
+
expect(server.instance_variable_get(:@socket_name)).to eq('test.sock')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'handles URLs with fragments' do
|
|
80
|
+
server = UnixSocks.from_url('tcp://localhost:8080#fragment')
|
|
81
|
+
expect(server).to be_a(UnixSocks::TCPSocketServer)
|
|
82
|
+
expect(server.instance_variable_get(:@hostname)).to eq('localhost')
|
|
83
|
+
expect(server.instance_variable_get(:@port)).to eq(8080)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe 'integration test' do
|
|
89
|
+
it 'can create and use unix server from URL' do
|
|
90
|
+
server = UnixSocks.from_url('unix:///tmp/test.sock')
|
|
91
|
+
expect(server).to be_a(UnixSocks::DomainSocketServer)
|
|
92
|
+
expect(server.to_url).to match(%r{^unix://.*test\.sock$})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'can create and use tcp server from URL' do
|
|
96
|
+
server = UnixSocks.from_url('tcp://localhost:8080')
|
|
97
|
+
expect(server).to be_a(UnixSocks::TCPSocketServer)
|
|
98
|
+
expect(server.to_url).to eq('tcp://localhost:8080')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/unix_socks.gemspec
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: unix_socks 0.
|
|
2
|
+
# stub: unix_socks 0.3.0 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "unix_socks".freeze
|
|
6
|
-
s.version = "0.
|
|
6
|
+
s.version = "0.3.0".freeze
|
|
7
7
|
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
9
9
|
s.require_paths = ["lib".freeze]
|
|
@@ -11,15 +11,15 @@ Gem::Specification.new do |s|
|
|
|
11
11
|
s.date = "1980-01-02"
|
|
12
12
|
s.description = "This library enables communication between processes using Unix sockets. It\nhandles message transmission, socket management, and cleanup, supporting\nboth synchronous and asynchronous operations while providing error handling\nfor robust development.\n".freeze
|
|
13
13
|
s.email = "flori@ping.de".freeze
|
|
14
|
-
s.extra_rdoc_files = ["README.md".freeze, "lib/unix_socks.rb".freeze, "lib/unix_socks/message.rb".freeze, "lib/unix_socks/
|
|
15
|
-
s.files = ["CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "lib/unix_socks.rb".freeze, "lib/unix_socks/message.rb".freeze, "lib/unix_socks/
|
|
14
|
+
s.extra_rdoc_files = ["README.md".freeze, "lib/unix_socks.rb".freeze, "lib/unix_socks/domain_socket_server.rb".freeze, "lib/unix_socks/message.rb".freeze, "lib/unix_socks/server_error.rb".freeze, "lib/unix_socks/server_shared.rb".freeze, "lib/unix_socks/tcp_socket_server.rb".freeze, "lib/unix_socks/version.rb".freeze]
|
|
15
|
+
s.files = ["CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "lib/unix_socks.rb".freeze, "lib/unix_socks/domain_socket_server.rb".freeze, "lib/unix_socks/message.rb".freeze, "lib/unix_socks/server_error.rb".freeze, "lib/unix_socks/server_shared.rb".freeze, "lib/unix_socks/tcp_socket_server.rb".freeze, "lib/unix_socks/version.rb".freeze, "spec/spec_helper.rb".freeze, "spec/unix_socks/domain_socket_server_spec.rb".freeze, "spec/unix_socks/message_spec.rb".freeze, "spec/unix_socks/server_interface_spec.rb".freeze, "spec/unix_socks/tcp_socket_server_spec.rb".freeze, "spec/unix_socks_spec.rb".freeze, "unix_socks.gemspec".freeze]
|
|
16
16
|
s.homepage = "https://github.com/flori/unix_socks".freeze
|
|
17
17
|
s.licenses = ["MIT".freeze]
|
|
18
18
|
s.rdoc_options = ["--title".freeze, "UnixSocks - A Ruby library for inter-process communication via Unix sockets with\ndynamic message handling\n".freeze, "--main".freeze, "README.md".freeze]
|
|
19
19
|
s.required_ruby_version = Gem::Requirement.new(">= 3.1".freeze)
|
|
20
20
|
s.rubygems_version = "4.0.2".freeze
|
|
21
21
|
s.summary = "A Ruby library for inter-process communication via Unix sockets with dynamic message handling".freeze
|
|
22
|
-
s.test_files = ["spec/spec_helper.rb".freeze, "spec/unix_socks/message_spec.rb".freeze, "spec/unix_socks/
|
|
22
|
+
s.test_files = ["spec/spec_helper.rb".freeze, "spec/unix_socks/domain_socket_server_spec.rb".freeze, "spec/unix_socks/message_spec.rb".freeze, "spec/unix_socks/server_interface_spec.rb".freeze, "spec/unix_socks/tcp_socket_server_spec.rb".freeze, "spec/unix_socks_spec.rb".freeze]
|
|
23
23
|
|
|
24
24
|
s.specification_version = 4
|
|
25
25
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unix_socks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Frank
|
|
@@ -132,8 +132,11 @@ extensions: []
|
|
|
132
132
|
extra_rdoc_files:
|
|
133
133
|
- README.md
|
|
134
134
|
- lib/unix_socks.rb
|
|
135
|
+
- lib/unix_socks/domain_socket_server.rb
|
|
135
136
|
- lib/unix_socks/message.rb
|
|
136
|
-
- lib/unix_socks/
|
|
137
|
+
- lib/unix_socks/server_error.rb
|
|
138
|
+
- lib/unix_socks/server_shared.rb
|
|
139
|
+
- lib/unix_socks/tcp_socket_server.rb
|
|
137
140
|
- lib/unix_socks/version.rb
|
|
138
141
|
files:
|
|
139
142
|
- CHANGES.md
|
|
@@ -142,12 +145,18 @@ files:
|
|
|
142
145
|
- README.md
|
|
143
146
|
- Rakefile
|
|
144
147
|
- lib/unix_socks.rb
|
|
148
|
+
- lib/unix_socks/domain_socket_server.rb
|
|
145
149
|
- lib/unix_socks/message.rb
|
|
146
|
-
- lib/unix_socks/
|
|
150
|
+
- lib/unix_socks/server_error.rb
|
|
151
|
+
- lib/unix_socks/server_shared.rb
|
|
152
|
+
- lib/unix_socks/tcp_socket_server.rb
|
|
147
153
|
- lib/unix_socks/version.rb
|
|
148
154
|
- spec/spec_helper.rb
|
|
155
|
+
- spec/unix_socks/domain_socket_server_spec.rb
|
|
149
156
|
- spec/unix_socks/message_spec.rb
|
|
150
|
-
- spec/unix_socks/
|
|
157
|
+
- spec/unix_socks/server_interface_spec.rb
|
|
158
|
+
- spec/unix_socks/tcp_socket_server_spec.rb
|
|
159
|
+
- spec/unix_socks_spec.rb
|
|
151
160
|
- unix_socks.gemspec
|
|
152
161
|
homepage: https://github.com/flori/unix_socks
|
|
153
162
|
licenses:
|
|
@@ -179,5 +188,8 @@ summary: A Ruby library for inter-process communication via Unix sockets with dy
|
|
|
179
188
|
message handling
|
|
180
189
|
test_files:
|
|
181
190
|
- spec/spec_helper.rb
|
|
191
|
+
- spec/unix_socks/domain_socket_server_spec.rb
|
|
182
192
|
- spec/unix_socks/message_spec.rb
|
|
183
|
-
- spec/unix_socks/
|
|
193
|
+
- spec/unix_socks/server_interface_spec.rb
|
|
194
|
+
- spec/unix_socks/tcp_socket_server_spec.rb
|
|
195
|
+
- spec/unix_socks_spec.rb
|