fractor 0.1.4 → 0.1.6
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/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +162 -46
- data/README.adoc +1364 -376
- data/examples/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/pipeline_processing/pipeline_processing.rb +15 -15
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/specialized_workers.rb +44 -37
- data/lib/fractor/continuous_server.rb +188 -0
- data/lib/fractor/result_aggregator.rb +1 -1
- data/lib/fractor/supervisor.rb +277 -104
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work_queue.rb +68 -0
- data/lib/fractor/work_result.rb +1 -1
- data/lib/fractor/worker.rb +2 -1
- data/lib/fractor/wrapped_ractor.rb +12 -2
- data/lib/fractor.rb +2 -0
- metadata +15 -2
|
@@ -86,8 +86,8 @@ puts
|
|
|
86
86
|
|
|
87
87
|
supervisor1 = Fractor::Supervisor.new(
|
|
88
88
|
worker_pools: [
|
|
89
|
-
{ worker_class: ComputeWorker } # No num_workers specified
|
|
90
|
-
]
|
|
89
|
+
{ worker_class: ComputeWorker }, # No num_workers specified
|
|
90
|
+
],
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
# Add work items
|
|
@@ -97,7 +97,7 @@ supervisor1.add_work_items(work_items)
|
|
|
97
97
|
puts "Processing 10 work items with auto-detected workers..."
|
|
98
98
|
supervisor1.run
|
|
99
99
|
|
|
100
|
-
puts "Results: #{supervisor1.results.results.map(&:result).sort.join(
|
|
100
|
+
puts "Results: #{supervisor1.results.results.map(&:result).sort.join(', ')}"
|
|
101
101
|
puts "✓ Auto-detection successful!"
|
|
102
102
|
puts
|
|
103
103
|
|
|
@@ -110,8 +110,8 @@ puts
|
|
|
110
110
|
|
|
111
111
|
supervisor2 = Fractor::Supervisor.new(
|
|
112
112
|
worker_pools: [
|
|
113
|
-
{ worker_class: ComputeWorker, num_workers: 4 }
|
|
114
|
-
]
|
|
113
|
+
{ worker_class: ComputeWorker, num_workers: 4 },
|
|
114
|
+
],
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
supervisor2.add_work_items((11..20).map { |i| ComputeWork.new(i) })
|
|
@@ -119,7 +119,7 @@ supervisor2.add_work_items((11..20).map { |i| ComputeWork.new(i) })
|
|
|
119
119
|
puts "Processing 10 work items with 4 explicitly configured workers..."
|
|
120
120
|
supervisor2.run
|
|
121
121
|
|
|
122
|
-
puts "Results: #{supervisor2.results.results.map(&:result).sort.join(
|
|
122
|
+
puts "Results: #{supervisor2.results.results.map(&:result).sort.join(', ')}"
|
|
123
123
|
puts "✓ Explicit configuration successful!"
|
|
124
124
|
puts
|
|
125
125
|
|
|
@@ -135,8 +135,8 @@ puts
|
|
|
135
135
|
supervisor3 = Fractor::Supervisor.new(
|
|
136
136
|
worker_pools: [
|
|
137
137
|
{ worker_class: ComputeWorker }, # Auto-detected
|
|
138
|
-
{ worker_class: ComputeWorker, num_workers: 2 } # Explicit
|
|
139
|
-
]
|
|
138
|
+
{ worker_class: ComputeWorker, num_workers: 2 }, # Explicit
|
|
139
|
+
],
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
supervisor3.add_work_items((21..30).map { |i| ComputeWork.new(i) })
|
|
@@ -144,7 +144,7 @@ supervisor3.add_work_items((21..30).map { |i| ComputeWork.new(i) })
|
|
|
144
144
|
puts "Processing 10 work items with mixed configuration..."
|
|
145
145
|
supervisor3.run
|
|
146
146
|
|
|
147
|
-
puts "Results: #{supervisor3.results.results.map(&:result).sort.join(
|
|
147
|
+
puts "Results: #{supervisor3.results.results.map(&:result).sort.join(', ')}"
|
|
148
148
|
puts "✓ Mixed configuration successful!"
|
|
149
149
|
puts
|
|
150
150
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module ContinuousChat
|
|
8
|
+
# Message packet class for handling protocol messages
|
|
9
|
+
class MessagePacket
|
|
10
|
+
attr_reader :type, :data, :timestamp
|
|
11
|
+
|
|
12
|
+
def initialize(type, data, timestamp = Time.now.to_i)
|
|
13
|
+
@type = type.to_sym
|
|
14
|
+
@data = data
|
|
15
|
+
@timestamp = timestamp
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convert to JSON string
|
|
19
|
+
def to_json(*_args)
|
|
20
|
+
{
|
|
21
|
+
type: @type,
|
|
22
|
+
data: @data,
|
|
23
|
+
timestamp: @timestamp,
|
|
24
|
+
}.to_json
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# String representation
|
|
28
|
+
def to_s
|
|
29
|
+
to_json
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Helper module for message protocol
|
|
34
|
+
module MessageProtocol
|
|
35
|
+
# Create a packet of the given type with data
|
|
36
|
+
def self.create_packet(type, data)
|
|
37
|
+
MessagePacket.new(type, data).to_json
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parse a JSON string into a message packet
|
|
41
|
+
def self.parse_packet(json_string)
|
|
42
|
+
data = JSON.parse(json_string)
|
|
43
|
+
type = data["type"]&.to_sym
|
|
44
|
+
content = data["data"]
|
|
45
|
+
timestamp = data["timestamp"] || Time.now.to_i
|
|
46
|
+
|
|
47
|
+
MessagePacket.new(type, content, timestamp)
|
|
48
|
+
rescue JSON::ParserError => e
|
|
49
|
+
puts "Error parsing message: #{e.message}"
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
= Continuous Chat Server Example (Fractor-based)
|
|
2
|
+
|
|
3
|
+
== Overview
|
|
4
|
+
|
|
5
|
+
This example demonstrates Fractor's continuous mode feature with a chat server implementation. Unlike the plain socket implementation in `examples/continuous_chat_server/`, this version uses Fractor's Worker and Supervisor classes to process chat messages concurrently.
|
|
6
|
+
|
|
7
|
+
The example shows how to:
|
|
8
|
+
|
|
9
|
+
* Use Fractor in continuous mode (`continuous_mode: true`)
|
|
10
|
+
* Register work source callbacks with `register_work_source`
|
|
11
|
+
* Process messages asynchronously using Ractor-based workers
|
|
12
|
+
* Coordinate between main thread socket handling and Fractor workers
|
|
13
|
+
* Implement graceful shutdown
|
|
14
|
+
|
|
15
|
+
== Key Concepts
|
|
16
|
+
|
|
17
|
+
* *Continuous Mode*: The Fractor supervisor runs indefinitely, processing work as it arrives
|
|
18
|
+
* *Work Sources*: Callback functions that provide new work items to the supervisor on demand
|
|
19
|
+
* *Asynchronous Processing*: ChatWorker processes messages concurrently in Ractors
|
|
20
|
+
* *Thread Coordination*: Multiple threads working together - main thread handles I/O, Fractor workers process messages
|
|
21
|
+
* *Message Logging*: All message processing is logged to demonstrate Fractor's work distribution
|
|
22
|
+
|
|
23
|
+
== Architecture
|
|
24
|
+
|
|
25
|
+
=== Fractor Components
|
|
26
|
+
|
|
27
|
+
The server uses the following Fractor components:
|
|
28
|
+
|
|
29
|
+
1. *ChatMessage (Fractor::Work)*: Represents a chat message as a unit of work
|
|
30
|
+
- Encapsulates the message packet and optional client socket reference
|
|
31
|
+
- Each message becomes a work item in the Fractor queue
|
|
32
|
+
|
|
33
|
+
2. *ChatWorker (Fractor::Worker)*: Processes chat messages
|
|
34
|
+
- Runs in a Ractor for parallel processing
|
|
35
|
+
- Handles different message types (broadcast, direct_message, server_message, user_list)
|
|
36
|
+
- Returns WorkResult with processing outcome
|
|
37
|
+
|
|
38
|
+
3. *Supervisor*: Orchestrates the workers
|
|
39
|
+
- Configured with `continuous_mode: true` to run indefinitely
|
|
40
|
+
- Uses 2 worker Ractors (auto-detected from system processors)
|
|
41
|
+
- Registered work source pulls from a thread-safe Queue
|
|
42
|
+
|
|
43
|
+
=== Thread Architecture
|
|
44
|
+
|
|
45
|
+
The server uses three concurrent components:
|
|
46
|
+
|
|
47
|
+
1. *Main Thread*: Handles socket I/O with `IO.select`
|
|
48
|
+
- Accepts new client connections
|
|
49
|
+
- Reads messages from client sockets
|
|
50
|
+
- Sends responses back to clients
|
|
51
|
+
- Puts received messages into the work queue
|
|
52
|
+
|
|
53
|
+
2. *Supervisor Thread*: Runs the Fractor supervisor
|
|
54
|
+
- Continuously pulls work from the queue via the work source callback
|
|
55
|
+
- Distributes work to available ChatWorker Ractors
|
|
56
|
+
- Collects results in the ResultAggregator
|
|
57
|
+
|
|
58
|
+
3. *Results Thread*: Processes completed work
|
|
59
|
+
- Monitors the ResultAggregator for new results
|
|
60
|
+
- Logs processing outcomes
|
|
61
|
+
- Handles errors from workers
|
|
62
|
+
|
|
63
|
+
== Example Components
|
|
64
|
+
|
|
65
|
+
1. *chat_common.rb*: Shared code with Fractor classes
|
|
66
|
+
* `MessagePacket` class for message protocol
|
|
67
|
+
* `MessageProtocol` module for serialization
|
|
68
|
+
* `ChatMessage` class extending `Fractor::Work`
|
|
69
|
+
* `ChatWorker` class extending `Fractor::Worker`
|
|
70
|
+
|
|
71
|
+
2. *chat_server.rb*: Fractor-based chat server
|
|
72
|
+
* Socket handling in main thread
|
|
73
|
+
* Fractor supervisor in continuous mode
|
|
74
|
+
* Work source callback pulling from Queue
|
|
75
|
+
* Results processing thread
|
|
76
|
+
|
|
77
|
+
3. *chat_client.rb*: Simple chat client (reused from plain example)
|
|
78
|
+
* Connects to server via TCP socket
|
|
79
|
+
* Sends and receives JSON messages
|
|
80
|
+
|
|
81
|
+
4. *simulate.rb*: Automated simulation
|
|
82
|
+
* Creates server and multiple clients as processes
|
|
83
|
+
* Runs predefined message schedule
|
|
84
|
+
* Analyzes logs after completion
|
|
85
|
+
|
|
86
|
+
== Running the Example
|
|
87
|
+
|
|
88
|
+
=== Running the Simulation
|
|
89
|
+
|
|
90
|
+
To run the complete automated simulation:
|
|
91
|
+
|
|
92
|
+
[source,sh]
|
|
93
|
+
----
|
|
94
|
+
ruby examples/continuous_chat_fractor/simulate.rb
|
|
95
|
+
----
|
|
96
|
+
|
|
97
|
+
Optional parameters:
|
|
98
|
+
* `-p, --port PORT` - Specify server port (default: 3000)
|
|
99
|
+
* `-d, --duration SECONDS` - Duration of simulation in seconds (default: 10)
|
|
100
|
+
* `-l, --log-dir DIR` - Directory for log files (default: logs)
|
|
101
|
+
* `-h, --help` - Show help message
|
|
102
|
+
|
|
103
|
+
=== Running Server and Clients Separately
|
|
104
|
+
|
|
105
|
+
Run the server in one terminal:
|
|
106
|
+
|
|
107
|
+
[source,sh]
|
|
108
|
+
----
|
|
109
|
+
ruby examples/continuous_chat_fractor/chat_server.rb [PORT] [LOG_FILE]
|
|
110
|
+
----
|
|
111
|
+
|
|
112
|
+
Run clients in different terminals:
|
|
113
|
+
|
|
114
|
+
[source,sh]
|
|
115
|
+
----
|
|
116
|
+
ruby examples/continuous_chat_fractor/chat_client.rb [USERNAME] [PORT] [LOG_FILE]
|
|
117
|
+
----
|
|
118
|
+
|
|
119
|
+
== Features Demonstrated
|
|
120
|
+
|
|
121
|
+
* *Continuous Mode*: Supervisor runs indefinitely without stopping
|
|
122
|
+
* *Work Source Callback*: Dynamically provides work from a Queue
|
|
123
|
+
* *Concurrent Processing*: Multiple Ractor workers process messages in parallel
|
|
124
|
+
* *Thread Coordination*: Main thread, supervisor thread, and results thread work together
|
|
125
|
+
* *Message Logging*: All operations logged to files for verification
|
|
126
|
+
* *Graceful Shutdown*: Proper cleanup of Fractor supervisor and sockets
|
|
127
|
+
|
|
128
|
+
== Comparison with Plain Socket Implementation
|
|
129
|
+
|
|
130
|
+
The plain socket implementation (`examples/continuous_chat_server/`) uses:
|
|
131
|
+
- `IO.select` for non-blocking I/O
|
|
132
|
+
- Sequential message processing in the main thread
|
|
133
|
+
- Simple, straightforward architecture
|
|
134
|
+
|
|
135
|
+
The Fractor-based implementation demonstrates:
|
|
136
|
+
- Parallel message processing using Ractors
|
|
137
|
+
- Work queue pattern with work source callbacks
|
|
138
|
+
- Separation of concerns (I/O vs processing)
|
|
139
|
+
- Continuous mode supervisor pattern
|
|
140
|
+
|
|
141
|
+
Both implementations are functional. The Fractor version shows how to structure a long-running server using Fractor's continuous mode, which is useful for:
|
|
142
|
+
- CPU-intensive message processing
|
|
143
|
+
- Scaling message handling across cores
|
|
144
|
+
- Separating I/O from computation
|
|
145
|
+
- Learning Fractor's continuous mode patterns
|
|
146
|
+
|
|
147
|
+
== Expected Output
|
|
148
|
+
|
|
149
|
+
The simulation will show:
|
|
150
|
+
* Fractor supervisor starting with workers
|
|
151
|
+
* Clients connecting to the server
|
|
152
|
+
* Messages being sent between clients
|
|
153
|
+
* Messages being added to Fractor work queue (logged as "Received from...")
|
|
154
|
+
* Graceful shutdown of all components
|
|
155
|
+
|
|
156
|
+
NOTE: In this implementation, Fractor workers process messages in parallel for demonstration purposes (analyzing message types, logging processing), while the main thread handles actual message delivery to ensure real-time responsiveness. The work items are successfully queued and processed by workers - you can verify this by seeing that all messages are correctly broadcast/delivered to clients.
|
|
157
|
+
|
|
158
|
+
== Log Files
|
|
159
|
+
|
|
160
|
+
After running the simulation, check the `logs/` directory:
|
|
161
|
+
|
|
162
|
+
* `server_messages.log` - Server activity and Fractor processing
|
|
163
|
+
* `client_<username>_messages.log` - Client activity
|
|
164
|
+
* `client_<username>_send_messages.json` - Messages sent by client
|
|
165
|
+
|
|
166
|
+
== Implementation Notes
|
|
167
|
+
|
|
168
|
+
=== Why Thread-safe Queue?
|
|
169
|
+
|
|
170
|
+
The implementation uses Ruby's `Queue` class (thread-safe) to coordinate between:
|
|
171
|
+
- Main thread (producing work from socket I/O)
|
|
172
|
+
- Supervisor thread (consuming work via work source callback)
|
|
173
|
+
|
|
174
|
+
This is necessary because Ractors cannot directly share mutable objects with threads.
|
|
175
|
+
|
|
176
|
+
=== Work Source Callback
|
|
177
|
+
|
|
178
|
+
The work source callback pulls up to 5 messages from the queue at once:
|
|
179
|
+
|
|
180
|
+
[source,ruby]
|
|
181
|
+
----
|
|
182
|
+
supervisor.register_work_source do
|
|
183
|
+
messages = []
|
|
184
|
+
5.times do
|
|
185
|
+
break if message_queue.empty?
|
|
186
|
+
msg = message_queue.pop(true) rescue nil
|
|
187
|
+
messages << msg if msg
|
|
188
|
+
end
|
|
189
|
+
messages.empty? ? nil : messages
|
|
190
|
+
end
|
|
191
|
+
----
|
|
192
|
+
|
|
193
|
+
This batching improves efficiency by reducing callback overhead.
|
|
194
|
+
|
|
195
|
+
=== Results Processing
|
|
196
|
+
|
|
197
|
+
A separate thread monitors the ResultAggregator because:
|
|
198
|
+
- The main thread is busy with socket I/O
|
|
199
|
+
- The supervisor thread is running `supervisor.run`
|
|
200
|
+
- We want to log results as they complete
|
|
201
|
+
|
|
202
|
+
In a production system, you might process results differently (e.g., send notifications, update databases, etc.).
|
|
203
|
+
|
|
204
|
+
== Continuous Mode Benefits
|
|
205
|
+
|
|
206
|
+
This example demonstrates key benefits of Fractor's continuous mode:
|
|
207
|
+
|
|
208
|
+
1. *Non-stopping Execution*: Server runs indefinitely, processing messages as they arrive
|
|
209
|
+
2. *Dynamic Work Addition*: Work source callback provides new work on demand
|
|
210
|
+
3. *Resource Efficiency*: Workers idle when no work available
|
|
211
|
+
4. *Parallel Processing*: Multiple messages processed concurrently
|
|
212
|
+
5. *Graceful Shutdown*: `supervisor.stop` cleanly terminates workers
|
|
213
|
+
|
|
214
|
+
== See Also
|
|
215
|
+
|
|
216
|
+
* link:../continuous_chat_server/[Plain Socket Implementation] - Simpler approach without Fractor
|
|
217
|
+
* link:../../README.adoc#continuous-mode[Main README Continuous Mode Section]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module ContinuousChat
|
|
9
|
+
# Simple Chat Client using Ruby's standard socket library
|
|
10
|
+
class ChatClient
|
|
11
|
+
def initialize(username, server_host = 'localhost', server_port = 3000,
|
|
12
|
+
log_file_path = nil)
|
|
13
|
+
@username = username
|
|
14
|
+
@server_host = server_host
|
|
15
|
+
@server_port = server_port
|
|
16
|
+
@running = true
|
|
17
|
+
|
|
18
|
+
# Set up logging
|
|
19
|
+
@log_file_path = log_file_path || "logs/client_#{username}_messages.log"
|
|
20
|
+
FileUtils.mkdir_p(File.dirname(@log_file_path))
|
|
21
|
+
@log_file = File.open(@log_file_path, 'w')
|
|
22
|
+
|
|
23
|
+
log_message("Client initialized for #{username}, connecting to #{server_host}:#{server_port}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def connect
|
|
27
|
+
puts "Connecting to server at #{@server_host}:#{@server_port}..."
|
|
28
|
+
log_message("Connecting to server at #{@server_host}:#{@server_port}")
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
@socket = TCPSocket.new(@server_host, @server_port)
|
|
32
|
+
|
|
33
|
+
# Send join message
|
|
34
|
+
join_data = {
|
|
35
|
+
type: 'join',
|
|
36
|
+
data: {
|
|
37
|
+
username: @username
|
|
38
|
+
},
|
|
39
|
+
timestamp: Time.now.to_i
|
|
40
|
+
}
|
|
41
|
+
@socket.puts(JSON.generate(join_data))
|
|
42
|
+
log_message("Sent join message: #{join_data}")
|
|
43
|
+
|
|
44
|
+
puts 'Connected to chat server!'
|
|
45
|
+
log_message('Connected to chat server')
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
puts "Failed to connect: #{e.message}"
|
|
50
|
+
log_message("Failed to connect: #{e.message}")
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start
|
|
56
|
+
return false unless @socket
|
|
57
|
+
|
|
58
|
+
# Main event loop that handles both sending and receiving messages
|
|
59
|
+
# without creating separate threads
|
|
60
|
+
main_event_loop
|
|
61
|
+
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Main event loop that handles both user input and server messages
|
|
66
|
+
def main_event_loop
|
|
67
|
+
log_message('Starting main event loop')
|
|
68
|
+
|
|
69
|
+
# Print initial prompt
|
|
70
|
+
print "(#{@username})> "
|
|
71
|
+
$stdout.flush
|
|
72
|
+
|
|
73
|
+
while @running
|
|
74
|
+
# Use IO.select to wait for input from either STDIN or the socket
|
|
75
|
+
# This is non-blocking and allows us to handle both in a single loop
|
|
76
|
+
readable, = IO.select([@socket, $stdin], nil, nil, 0.1)
|
|
77
|
+
|
|
78
|
+
next unless readable # Nothing to process this iteration
|
|
79
|
+
|
|
80
|
+
readable.each do |io|
|
|
81
|
+
if io == $stdin
|
|
82
|
+
# Handle user input
|
|
83
|
+
handle_user_input
|
|
84
|
+
elsif io == @socket
|
|
85
|
+
# Handle server message
|
|
86
|
+
handle_server_message
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
rescue Interrupt
|
|
91
|
+
log_message('Client interrupted')
|
|
92
|
+
@running = false
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
log_message("Error in main event loop: #{e.message}")
|
|
95
|
+
@running = false
|
|
96
|
+
ensure
|
|
97
|
+
disconnect
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Handle user input (non-blocking)
|
|
101
|
+
def handle_user_input
|
|
102
|
+
text = $stdin.gets&.chomp
|
|
103
|
+
return unless text
|
|
104
|
+
|
|
105
|
+
# Check if client wants to quit
|
|
106
|
+
if text == '/quit' || text.nil?
|
|
107
|
+
@running = false
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Create message packet
|
|
112
|
+
message_data = {
|
|
113
|
+
type: 'message',
|
|
114
|
+
data: {
|
|
115
|
+
content: text,
|
|
116
|
+
recipient: 'all' # Default to broadcast
|
|
117
|
+
},
|
|
118
|
+
timestamp: Time.now.to_i
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Send to server
|
|
122
|
+
@socket.puts(JSON.generate(message_data))
|
|
123
|
+
log_message("Sent message: #{text}")
|
|
124
|
+
|
|
125
|
+
# Print prompt for next input
|
|
126
|
+
print "(#{@username})> "
|
|
127
|
+
$stdout.flush
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
log_message("Error handling user input: #{e.message}")
|
|
130
|
+
@running = false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Handle server message (non-blocking)
|
|
134
|
+
def handle_server_message
|
|
135
|
+
line = @socket.gets&.chomp
|
|
136
|
+
|
|
137
|
+
if line.nil?
|
|
138
|
+
# Server closed the connection
|
|
139
|
+
log_message('Connection to server lost')
|
|
140
|
+
@running = false
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parse and handle the message
|
|
145
|
+
message = JSON.parse(line)
|
|
146
|
+
log_message("Received: #{line}")
|
|
147
|
+
|
|
148
|
+
# Display formatted message based on type
|
|
149
|
+
case message['type']
|
|
150
|
+
when 'broadcast'
|
|
151
|
+
puts "\r#{message['data']['from']}: #{message['data']['content']}"
|
|
152
|
+
when 'direct_message'
|
|
153
|
+
puts "\r[DM] #{message['data']['from']}: #{message['data']['content']}"
|
|
154
|
+
when 'server_message'
|
|
155
|
+
puts "\r[Server] #{message['data']['message']}"
|
|
156
|
+
when 'user_list'
|
|
157
|
+
puts "\r[Server] Users online: #{message['data']['users'].join(', ')}"
|
|
158
|
+
when 'error'
|
|
159
|
+
puts "\r[Error] #{message['data']['message']}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Reprint the prompt
|
|
163
|
+
print "(#{@username})> "
|
|
164
|
+
$stdout.flush
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
log_message("Error handling server message: #{e.message}")
|
|
167
|
+
# Don't immediately break for connection errors, may be temporary
|
|
168
|
+
# Just log and continue, IO.select will catch closed connections
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def run_with_messages(messages, _delay_between_messages = 1)
|
|
172
|
+
return false unless @socket && @running
|
|
173
|
+
|
|
174
|
+
log_message("Running with #{messages.size} predefined messages")
|
|
175
|
+
puts "Sending #{messages.size} predefined messages"
|
|
176
|
+
|
|
177
|
+
# Send all messages in a non-blocking way
|
|
178
|
+
batch_send_messages(messages)
|
|
179
|
+
|
|
180
|
+
log_message('Finished sending all predefined messages')
|
|
181
|
+
|
|
182
|
+
# Start the event loop to receive responses
|
|
183
|
+
main_event_loop
|
|
184
|
+
|
|
185
|
+
true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Helper to send a batch of messages without blocking
|
|
189
|
+
def batch_send_messages(messages)
|
|
190
|
+
messages.each_with_index do |msg, index|
|
|
191
|
+
content = msg[:content]
|
|
192
|
+
recipient = msg[:recipient] || 'all'
|
|
193
|
+
|
|
194
|
+
log_message("Sending message #{index + 1}: '#{content}' to #{recipient}")
|
|
195
|
+
|
|
196
|
+
message_data = {
|
|
197
|
+
type: 'message',
|
|
198
|
+
data: {
|
|
199
|
+
content: content,
|
|
200
|
+
recipient: recipient
|
|
201
|
+
},
|
|
202
|
+
timestamp: Time.now.to_i
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@socket.puts(JSON.generate(message_data))
|
|
206
|
+
log_message("Sent message to #{recipient}: #{content}")
|
|
207
|
+
|
|
208
|
+
# Small delay between messages for stability
|
|
209
|
+
sleep(0.1)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def disconnect
|
|
214
|
+
return unless @running
|
|
215
|
+
|
|
216
|
+
@running = false
|
|
217
|
+
log_message('Disconnecting from server')
|
|
218
|
+
|
|
219
|
+
if @socket && !@socket.closed?
|
|
220
|
+
# Send leave message
|
|
221
|
+
leave_data = {
|
|
222
|
+
type: 'leave',
|
|
223
|
+
data: {
|
|
224
|
+
username: @username
|
|
225
|
+
},
|
|
226
|
+
timestamp: Time.now.to_i
|
|
227
|
+
}
|
|
228
|
+
@socket.puts(JSON.generate(leave_data))
|
|
229
|
+
log_message('Sent leave message')
|
|
230
|
+
|
|
231
|
+
@socket.close
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
@log_file&.close
|
|
235
|
+
|
|
236
|
+
puts 'Disconnected from server.'
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def log_message(message)
|
|
243
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
|
|
244
|
+
log_entry = "[#{timestamp}] #{message}"
|
|
245
|
+
|
|
246
|
+
@log_file.puts(log_entry)
|
|
247
|
+
@log_file.flush # Ensure it's written immediately
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# When run directly, start the client
|
|
253
|
+
if __FILE__ == $PROGRAM_NAME
|
|
254
|
+
require 'fileutils'
|
|
255
|
+
require 'json'
|
|
256
|
+
|
|
257
|
+
puts 'Chat Client'
|
|
258
|
+
puts '==========='
|
|
259
|
+
puts 'This is the chat client that connects to the chat server.'
|
|
260
|
+
puts 'All messages are logged to a file for later analysis.'
|
|
261
|
+
puts
|
|
262
|
+
|
|
263
|
+
# Get username from command line or prompt
|
|
264
|
+
username = ARGV[0]
|
|
265
|
+
|
|
266
|
+
unless username
|
|
267
|
+
print 'Enter your username: '
|
|
268
|
+
username = gets.chomp
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Get port from command line or use default
|
|
272
|
+
port = ARGV[1]&.to_i || 3000
|
|
273
|
+
log_file = ARGV[2] || "logs/client_#{username}_messages.log"
|
|
274
|
+
|
|
275
|
+
# Check for messages file
|
|
276
|
+
messages_file = "logs/client_#{username}_send_messages.json"
|
|
277
|
+
|
|
278
|
+
# Create and run the client
|
|
279
|
+
client = ContinuousChat::ChatClient.new(username, 'localhost', port, log_file)
|
|
280
|
+
|
|
281
|
+
if client.connect
|
|
282
|
+
begin
|
|
283
|
+
# Load and send messages if the file exists
|
|
284
|
+
if File.exist?(messages_file)
|
|
285
|
+
puts "Loading messages from #{messages_file}"
|
|
286
|
+
messages = JSON.parse(File.read(messages_file), symbolize_names: true)
|
|
287
|
+
puts "Loaded #{messages.size} messages"
|
|
288
|
+
|
|
289
|
+
# Send the messages
|
|
290
|
+
client.run_with_messages(messages)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Start the client
|
|
294
|
+
client.start
|
|
295
|
+
rescue Interrupt
|
|
296
|
+
puts "\nClient interrupted."
|
|
297
|
+
ensure
|
|
298
|
+
client.disconnect
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
puts 'Chat client exited.'
|
|
303
|
+
end
|