eksa-server 1.0.0 โ†’ 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20f80700434c2365febde215212050d0d859be1d8b67a2225506545b1ec84ded
4
- data.tar.gz: 0fbf23b72be466483c7cb579f11cf1f4c427d09a361df3199acf67567f3d64ae
3
+ metadata.gz: 72911bfe3a28c5d0e812c9dbbb7f993f4f09e409a4dc872999d059ac089fcba7
4
+ data.tar.gz: 4964e10492c37334d6729bbd38ce1a91b5c48ab1c2fef75ce5667a14c0a1e58f
5
5
  SHA512:
6
- metadata.gz: fa73c08ba2d35178db74d35e747906e0a9cabe1f8f0019ba3d516c6f71039798cfdd178a7d540b912f66616564f54e081f4f2ce9873c0cd4fb362f87c8a5ceab
7
- data.tar.gz: 6ba5333277149c9cf839368b244632759417315e9a050f5ca60f39567373d179f4c2d0c80b6ee5e083d8121206e7254b917179db19402af2ea16c080912e4e6f
6
+ metadata.gz: 6b2812653f57e1454481229021fb2ec846d2bc1e1591d60a4442811647a8ad04adb5e9d17ddd59913cedcd1291348e5d77eb9105bfc0083371e6a756b786e0de
7
+ data.tar.gz: f01bf24108df7967bf8b8fcc28633c2811c705e596674266d6ac956839a3681eae8bfe6df4ee085e958867504fb6f788bc985d1b544432d22457e6cf04634adc
data/README.md CHANGED
@@ -1,32 +1,27 @@
1
1
  # EksaServer
2
2
 
3
- **EksaServer** adalah server web Ruby asinkron berperforma tinggi. Dibangun di atas `nio4r`, server ini dirancang untuk menangani ribuan koneksi secara efisien dengan dukungan multithreading dan multiprocess (cluster mode).
3
+ **EksaServer** adalah server web Ruby asinkron berperforma tinggi yang dirancang untuk keandalan dan kecepatan. Dibangun di atas `nio4r`, server ini mampu menangani ribuan koneksi secara efisien melalui model multithreading dan multiprocess (cluster mode).
4
4
 
5
5
  ## Fitur Utama
6
6
 
7
- - ๐Ÿš€ **Performa Tinggi**: Menggunakan `nio4r` untuk event loop non-blocking.
8
- - ๐Ÿงต **Multithreading Dinamis**: Thread pool yang menskalakan secara otomatis sesuai beban.
9
- - ๐Ÿ—๏ธ **Cluster Mode**: Memanfaatkan multi-core CPU dengan worker process.
10
- - ๐Ÿ”„ **Phased Restarts**: Update kode aplikasi tanpa downtime (Zero Downtime).
11
- - ๐Ÿ› ๏ธ **Konfigurasi DSL**: Pengaturan mudah melalui file `server.rb`.
12
- - ๐Ÿ”Œ **Rack Compliant**: Mendukung semua framework Ruby berbasis Rack (Rails, Sinatra, EksaFramework, dll).
13
- - ๐Ÿ›ก๏ธ **Premium Error Page**: Halaman error *glassmorphism* yang elegan.
7
+ - ๐Ÿš€ **Performa Tinggi**: Event loop non-blocking berbasis `nio4r`.
8
+ - ๐Ÿงต **Auto-Scaling Threads**: Thread pool yang menyesuaikan diri dengan beban request.
9
+ - ๐Ÿ—๏ธ **Cluster Mode**: Worker process mandiri untuk memanfaatkan multi-core CPU.
10
+ - ๐Ÿ” **SSL/HTTPS Native**: Dukungan enkripsi SSL yang mudah via CLI atau config.
11
+ - ๐Ÿ”„ **Auto-Reload**: Pemuatan ulang otomatis saat ada perubahan kode (`--reload`).
12
+ - ๐Ÿ“Š **Control Server**: API statistik real-time (memory, workers, threads).
13
+ - ๐Ÿ› ๏ธ **Fleksibilitas Konfigurasi**: Mendukung DSL Ruby, variabel lingkungan (`.env`), dan opsi CLI.
14
+ - ๐Ÿ›ก๏ธ **Premium Error Page**: Tampilan error *glassmorphism* yang elegan dan informatif.
14
15
 
15
16
  ## Instalasi
16
17
 
17
- Tambahkan baris ini ke Gemfile aplikasi Anda:
18
+ Tambahkan ke Gemfile Anda:
18
19
 
19
20
  ```ruby
20
21
  gem 'eksa-server'
21
22
  ```
22
23
 
23
- Lalu jalankan:
24
-
25
- ```bash
26
- bundle install
27
- ```
28
-
29
- Atau instal langsung melalui terminal:
24
+ Atau instal langsung:
30
25
 
31
26
  ```bash
32
27
  gem install eksa-server
@@ -34,37 +29,65 @@ gem install eksa-server
34
29
 
35
30
  ## Penggunaan Cepat
36
31
 
37
- Cukup jalankan perintah berikut di direktori proyek Anda yang memiliki file `config.ru`:
32
+ Jalankan di direktori aplikasi Rack Anda:
38
33
 
39
34
  ```bash
40
35
  eksa-server
41
36
  ```
42
37
 
43
- Anda juga bisa menentukan file rack secara spesifik:
44
-
38
+ ### Opsi CLI yang Berguna
39
+
40
+ | Opsi | Deskripsi |
41
+ |------|-----------|
42
+ | `-p, --port` | Port server (default: 3000 atau dari `.env`) |
43
+ | `-o, --host` | Host untuk bind (default: 0.0.0.0) |
44
+ | `-b, --bind URL` | Tambahkan bind (tcp://host:port atau unix://path). Bisa dipanggil berkali-kali. |
45
+ | `-R, --reload` | Aktifkan auto-reload saat file `.rb` berubah |
46
+ | `-c, --control` | Port untuk Control Server (statistik) |
47
+ | `-D, --daemonize` | Berjalan di latar belakang (Daemon Mode) |
48
+ | `-L, --log PATH` | Simpan log ke file tertentu |
49
+ | `--ssl-cert PATH` | Path ke sertifikat SSL (.crt) |
50
+ | `--ssl-key PATH` | Path ke private key SSL (.key) |
51
+
52
+ Contoh penggunaan lengkap:
45
53
  ```bash
46
- eksa-server app/config.ru -p 4000
54
+ eksa-server config.ru -p 443 --ssl-cert server.crt --ssl-key server.key -w 4 -R
47
55
  ```
48
56
 
49
57
  ## Konfigurasi
50
58
 
51
- Buat file `config/eksa_server.rb` untuk pengaturan tingkat lanjut:
59
+ EksaServer otomatis memuat file `.env` jika tersedia. Anda juga bisa menggunakan file `config/eksa_server.rb`:
52
60
 
53
61
  ```ruby
62
+ # config/eksa_server.rb
54
63
  threads 5, 20 # Min, Max threads
55
- workers 2 # Aktifkan Cluster Mode
56
- bind "tcp://0.0.0.0:3000"
57
- # bind "unix:///tmp/eksa.sock"
64
+ workers 2 # Jumlah worker
65
+ control_port 3001 # API Statistik
66
+
67
+ # SSL (Opsional)
68
+ ssl true
69
+ cert "path/to/cert.crt"
70
+ key "path/to/key.key"
58
71
 
59
72
  on_worker_boot do |index|
60
- puts "Worker #{index} siap melayani!"
73
+ puts "Worker #{index} siap beraksi!"
61
74
  end
62
75
  ```
63
76
 
64
- ## Dokumentasi Lengkap
77
+ ## Statistik (Control Server)
65
78
 
66
- Untuk melihat panduan lengkap dan fitur interaktif, silakan kunjungi repositori resmi di GitHub atau jalankan secara lokal dari source code:
67
- [https://github.com/IshikawaUta/eksa-server](https://github.com/IshikawaUta/eksa-server)
79
+ Jika Control Server aktif, Anda bisa memantau kesehatan server via JSON:
80
+ `curl http://localhost:3001`
81
+
82
+ ```json
83
+ {
84
+ "workers": 2,
85
+ "uptime": 3600,
86
+ "version": "1.1.0",
87
+ "memory_kb": 45120,
88
+ "threads": { "spawned": 10, "waiting": 8 }
89
+ }
90
+ ```
68
91
 
69
92
  ## Lisensi
70
93
 
data/bin/eksa-server CHANGED
@@ -9,7 +9,21 @@ OptionParser.new do |opts|
9
9
  opts.banner = "Usage: eksa-server [options] [config.ru]"
10
10
 
11
11
  opts.on("-p", "--port PORT", "Port to bind to (default: 3000)") { |v| options[:port] = v.to_i }
12
+ opts.on("-o", "--host HOST", "Host to bind to (default: 0.0.0.0)") { |v| options[:host] = v }
13
+ opts.on("-b", "--bind URL", "Bind URL (tcp://host:port or unix://path)") do |v|
14
+ options[:binds] ||= []
15
+ options[:binds] << v
16
+ end
12
17
  opts.on("-w", "--workers COUNT", "Number of worker processes") { |v| options[:workers] = v.to_i }
18
+ opts.on("-t", "--timeout SECONDS", "Worker timeout in seconds") { |v| options[:timeout] = v.to_i }
19
+ opts.on("-c", "--control PORT", "Control server port (0 for random, false to disable)") do |v|
20
+ options[:control_port] = (v == "false" ? false : v.to_i)
21
+ end
22
+ opts.on("-L", "--log PATH", "Path to log file") { |v| options[:log_file] = v }
23
+ opts.on("-D", "--daemonize", "Run in background") { options[:daemonize] = true }
24
+ opts.on("-R", "--reload", "Auto-reload on file changes") { options[:reload] = true }
25
+ opts.on("--ssl-cert PATH", "Path to SSL certificate") { |v| options[:cert] = v; options[:ssl] = true }
26
+ opts.on("--ssl-key PATH", "Path to SSL private key") { |v| options[:key] = v; options[:ssl] = true }
13
27
  opts.on("-C", "--config PATH", "Path to config file") { |v| options[:config_file] = v }
14
28
  end.parse!
15
29
 
@@ -6,21 +6,37 @@ module EksaServer
6
6
  attr_reader :options
7
7
 
8
8
  def initialize(user_options = {})
9
+ load_env
9
10
  @options = default_options.merge(user_options)
10
11
  end
11
12
 
13
+ def load_env(path = '.env')
14
+ return unless File.exist?(path)
15
+ File.readlines(path).each do |line|
16
+ line = line.strip
17
+ next if line.empty? || line.start_with?('#')
18
+ key, value = line.split('=', 2)
19
+ next unless key && value
20
+ ENV[key] ||= value.gsub(/^["']|["']$/, '') # Bersihkan quote jika ada
21
+ end
22
+ end
23
+
12
24
  def load_config(path)
13
25
  return unless File.exist?(path)
14
26
  dsl = DSL.new(@options)
15
27
  dsl.instance_eval(File.read(path), path)
16
28
  end
17
29
 
30
+ def merge!(new_options)
31
+ @options.merge!(new_options)
32
+ end
33
+
18
34
  private
19
35
 
20
36
  def default_options
21
37
  {
22
- host: '0.0.0.0',
23
- port: 3000,
38
+ host: ENV['HOST'] || '0.0.0.0',
39
+ port: (ENV['PORT'] || 3000).to_i,
24
40
  min_threads: 5,
25
41
  max_threads: 16,
26
42
  workers: 0,
@@ -29,6 +29,11 @@ module EksaServer
29
29
  @stdout.puts msg
30
30
  end
31
31
 
32
+ def reopen(io)
33
+ @stdout = io
34
+ @logger = setup_logger
35
+ end
36
+
32
37
  private
33
38
 
34
39
  def setup_logger
@@ -0,0 +1,66 @@
1
+ # lib/eksa_server/http.rb
2
+ require 'stringio'
3
+
4
+ module EksaServer
5
+ class Request
6
+ attr_reader :env
7
+
8
+ def initialize(client, options = {})
9
+ @client = client
10
+ @options = options
11
+ @env = parse_request
12
+ end
13
+
14
+ private
15
+
16
+ def parse_request
17
+ lines = []
18
+ while (line = @client.gets) && line != "\r\n"
19
+ lines << line.chomp
20
+ end
21
+ return nil if lines.empty?
22
+
23
+ method, path, _version = lines.first.split(" ")
24
+ headers = lines[1..-1].each_with_object({}) do |line, h|
25
+ k, v = line.split(": ", 2)
26
+ h[k] = v
27
+ end
28
+
29
+ env = {
30
+ 'REQUEST_METHOD' => method,
31
+ 'SCRIPT_NAME' => '',
32
+ 'PATH_INFO' => path.split("?", 2)[0],
33
+ 'QUERY_STRING' => path.split("?", 2)[1] || "",
34
+ 'SERVER_NAME' => @options[:host],
35
+ 'SERVER_PORT' => @options[:port].to_s,
36
+ 'rack.version' => Rack::VERSION,
37
+ 'rack.url_scheme' => @options[:ssl] ? 'https' : 'http',
38
+ 'rack.input' => StringIO.new(""),
39
+ 'rack.errors' => $stderr,
40
+ 'rack.multithread' => true,
41
+ 'rack.multiprocess' => @options[:workers] > 0,
42
+ 'rack.run_once' => false
43
+ }
44
+
45
+ headers.each { |k, v| env["HTTP_#{k.upcase.gsub('-', '_')}"] = v }
46
+ env
47
+ end
48
+ end
49
+
50
+ class Response
51
+ def self.send_response(client, status, headers, body)
52
+ response = "HTTP/1.1 #{status} OK\r\n"
53
+ headers.each { |k, v| response << "#{k}: #{v}\r\n" }
54
+
55
+ full_body = ""
56
+ body = [body] unless body.respond_to?(:each)
57
+ body.each { |chunk| full_body << chunk }
58
+ body.close if body.respond_to?(:close)
59
+
60
+ response << "Content-Length: #{full_body.bytesize}\r\n\r\n"
61
+ response << full_body
62
+
63
+ client.write(response)
64
+ end
65
+ end
66
+ end
@@ -33,18 +33,20 @@ module EksaServer
33
33
  private
34
34
 
35
35
  def spawn_thread
36
- @spawned += 1
37
- thread = Thread.new do
38
- loop do
39
- @mutex.synchronize { @waiting += 1 }
40
- work = @todo.pop
41
- @mutex.synchronize { @waiting -= 1 }
42
- break if work == :exit
43
- @block.call(work) rescue nil
36
+ @mutex.synchronize do
37
+ @spawned += 1
38
+ thread = Thread.new do
39
+ loop do
40
+ @mutex.synchronize { @waiting += 1 }
41
+ work = @todo.pop
42
+ @mutex.synchronize { @waiting -= 1 }
43
+ break if work == :exit
44
+ @block.call(work) rescue nil
45
+ end
46
+ @mutex.synchronize { @spawned -= 1 }
44
47
  end
45
- @mutex.synchronize { @spawned -= 1 }
48
+ @pool << thread
46
49
  end
47
- @pool << thread
48
50
  end
49
51
  end
50
52
  end
@@ -1,4 +1,4 @@
1
1
  # lib/eksa_server/version.rb
2
2
  module EksaServer
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
  end
data/server.rb CHANGED
@@ -5,6 +5,7 @@ require 'thread'
5
5
  require 'json'
6
6
  require 'fileutils'
7
7
  require 'openssl'
8
+ require 'stringio'
8
9
  require 'rack'
9
10
  require 'rackup'
10
11
 
@@ -13,17 +14,42 @@ require_relative 'lib/eksa_server/events'
13
14
  require_relative 'lib/eksa_server/binder'
14
15
  require_relative 'lib/eksa_server/thread_pool'
15
16
  require_relative 'lib/eksa_server/configuration'
17
+ require_relative 'lib/eksa_server/http'
18
+ require_relative 'lib/eksa_server/version'
16
19
 
17
20
  class EksaServerCore
18
- def initialize(app, config = {})
19
- @app_path_or_obj = app
20
- @config = EksaServer::Configuration.new(config)
21
+ def initialize(app, user_options = {})
22
+ @app_path_or_obj = app.is_a?(String) ? File.expand_path(app) : app
23
+ @config = EksaServer::Configuration.new
24
+ @signal_queue = []
25
+ @project_root = Dir.pwd
21
26
 
22
- # Load optional config file
27
+ # 1. Load optional config file (priority paling rendah)
23
28
  if File.exist?('config/eksa_server.rb')
24
29
  @config.load_config('config/eksa_server.rb')
25
30
  end
26
31
 
32
+ # 2. Muat .env (menimpa config file jika ada variabelnya)
33
+ @config.load_env
34
+
35
+ # 3. Gabungkan user_options dari CLI (priority paling tinggi)
36
+ # Jika user memberikan port/host secara eksplisit di CLI atau ada di ENV,
37
+ # kita prioritaskan itu dan kosongkan binds agar tidak tumpang tindih.
38
+ cli_port = user_options[:port] || user_options[:host]
39
+ if cli_port || ENV['PORT'] || ENV['HOST']
40
+ @config.options[:binds] = []
41
+ @config.options[:port] = (user_options[:port] || ENV['PORT'] || @config.options[:port]).to_i
42
+ @config.options[:host] = user_options[:host] || ENV['HOST'] || @config.options[:host]
43
+ end
44
+ @config.merge!(user_options.compact)
45
+
46
+ # Expand paths to absolute versions before load_app changes Dir.chdir
47
+ [:cert, :key, :log_file].each do |opt|
48
+ if @config.options[opt] && @config.options[opt].is_a?(String)
49
+ @config.options[opt] = File.expand_path(@config.options[opt])
50
+ end
51
+ end
52
+
27
53
  @options = @config.options
28
54
  @events = EksaServer::Events.new($stdout, $stderr)
29
55
  @binder = EksaServer::Binder.new(@events)
@@ -36,6 +62,19 @@ class EksaServerCore
36
62
  end
37
63
 
38
64
  def start
65
+ if @options[:daemonize]
66
+ @events.info "Berjalan di latar belakang (Daemon Mode)..."
67
+ Process.daemon(true, true)
68
+ end
69
+
70
+ if @options[:log_file]
71
+ log_file = File.open(@options[:log_file], 'a')
72
+ log_file.sync = true
73
+ @events.reopen(log_file)
74
+ end
75
+
76
+ start_reloader if @options[:reload]
77
+
39
78
  print_banner
40
79
 
41
80
  # Bina Sockets (Binder)
@@ -86,13 +125,20 @@ class EksaServerCore
86
125
  @options[:workers].times { |i| spawn_worker(i) }
87
126
  @events.info "Cluster aktif dengan #{@options[:workers]} worker. ๐Ÿš€"
88
127
 
89
- trap('INT') { terminate_all }
90
- trap('TERM') { terminate_all }
91
- trap('USR2') { phased_restart }
128
+ trap('INT') { @signal_queue << :TERM }
129
+ trap('TERM') { @signal_queue << :TERM }
130
+ trap('USR2') { @signal_queue << :USR2 }
92
131
 
93
132
  loop do
133
+ case @signal_queue.shift
134
+ when :TERM
135
+ terminate_all
136
+ when :USR2
137
+ phased_restart
138
+ end
139
+
94
140
  check_workers
95
- sleep 2
141
+ sleep 1
96
142
  end
97
143
  end
98
144
 
@@ -120,6 +166,7 @@ class EksaServerCore
120
166
 
121
167
  def phased_restart
122
168
  @events.info "\e[35mMemulai Phased Restart...\e[0m"
169
+ @app = load_app(@app_path_or_obj)
123
170
  @worker_pids.each do |index, pid|
124
171
  Process.kill('TERM', pid)
125
172
  Process.wait(pid)
@@ -146,19 +193,42 @@ class EksaServerCore
146
193
  @selector.register(io, :r).value = :accept
147
194
  end
148
195
 
196
+ # Signal handling untuk single-worker mode
197
+ if @options[:workers] == 0
198
+ trap('INT') { @signal_queue << :TERM }
199
+ trap('TERM') { @signal_queue << :TERM }
200
+ end
201
+
149
202
  Thread.new do
150
203
  loop do
151
- FileUtils.touch("#{@hb_dir}/#{Process.pid}.hb")
204
+ begin
205
+ if File.directory?(@hb_dir)
206
+ FileUtils.touch("#{@hb_dir}/#{Process.pid}.hb")
207
+ end
208
+ rescue
209
+ # Abaikan error saat shutdown
210
+ end
152
211
  sleep 5
153
212
  end
154
213
  end
155
214
 
156
215
  loop do
157
- @selector.select(5) do |monitor|
216
+ if @signal_queue.include?(:TERM)
217
+ @events.info "Worker #{id} berhenti..."
218
+ break
219
+ end
220
+
221
+ @selector.select(1) do |monitor|
158
222
  if monitor.value == :accept
159
223
  begin
160
- client = monitor.io.accept_nonblock
224
+ if monitor.io.respond_to?(:accept_nonblock)
225
+ client = monitor.io.accept_nonblock
226
+ else
227
+ client = monitor.io.accept
228
+ end
161
229
  @selector.register(client, :r).value = :read
230
+ rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET => e
231
+ @events.warn "Gagal jabat tangan SSL/Koneksi terputus: #{e.message}"
162
232
  rescue IO::WaitReadable
163
233
  end
164
234
  else
@@ -175,36 +245,9 @@ class EksaServerCore
175
245
  end
176
246
 
177
247
  def process_client(client)
178
- # Baca request line & headers
179
- lines = []
180
- while (line = client.gets) && line != "\r\n"
181
- lines << line.chomp
182
- end
183
- return client.close if lines.empty?
184
-
185
- method, path, _version = lines.first.split(" ")
186
- headers = lines[1..-1].each_with_object({}) do |line, h|
187
- k, v = line.split(": ", 2)
188
- h[k] = v
189
- end
190
-
191
- env = {
192
- 'REQUEST_METHOD' => method,
193
- 'SCRIPT_NAME' => '',
194
- 'PATH_INFO' => path.split("?", 2)[0],
195
- 'QUERY_STRING' => path.split("?", 2)[1] || "",
196
- 'SERVER_NAME' => @options[:host],
197
- 'SERVER_PORT' => @options[:port].to_s,
198
- 'rack.version' => Rack::VERSION,
199
- 'rack.url_scheme' => @options[:ssl] ? 'https' : 'http',
200
- 'rack.input' => StringIO.new(""),
201
- 'rack.errors' => $stderr,
202
- 'rack.multithread' => true,
203
- 'rack.multiprocess' => @options[:workers] > 0,
204
- 'rack.run_once' => false
205
- }
206
-
207
- headers.each { |k, v| env["HTTP_#{k.upcase.gsub('-', '_')}"] = v }
248
+ request = EksaServer::Request.new(client, @options)
249
+ env = request.env
250
+ return client.close unless env
208
251
 
209
252
  # Panggil aplikasi Rack
210
253
  begin
@@ -216,20 +259,10 @@ class EksaServerCore
216
259
  res_body = [render_error_page(e)]
217
260
  end
218
261
 
219
- # Send Response
220
- response = "HTTP/1.1 #{status} OK\r\n"
221
- res_headers.each { |k, v| response << "#{k}: #{v}\r\n" }
262
+ # Kirim Response
263
+ EksaServer::Response.send_response(client, status, res_headers, res_body)
222
264
 
223
- full_body = ""
224
- res_body = [res_body] unless res_body.respond_to?(:each)
225
- res_body.each { |chunk| full_body << chunk }
226
- res_body.close if res_body.respond_to?(:close)
227
-
228
- response << "Content-Length: #{full_body.bytesize}\r\n\r\n"
229
- response << full_body
230
-
231
- client.write(response)
232
- @events.info "Selesai: #{method} #{path} -> #{status} (Threads: #{@thread_pool.spawned})"
265
+ @events.info "Selesai: #{env['REQUEST_METHOD']} #{env['PATH_INFO']} -> #{status} (Threads: #{@thread_pool.spawned})"
233
266
  client.close
234
267
  rescue => e
235
268
  @events.error "Kesalahan tingkat rendah: #{e.message}"
@@ -248,20 +281,66 @@ class EksaServerCore
248
281
  end
249
282
  end
250
283
 
284
+ def start_reloader
285
+ @events.info "Pemuatan otomatis (Auto-Reload) aktif. ๐Ÿ‘€"
286
+ Thread.new do
287
+ # Pantau file di project root agar mencakup lib dan server.rb
288
+ loop do
289
+ sleep 2
290
+ changed = false
291
+ files = Dir.chdir(@project_root) { Dir["**/*.{rb,ru}"] }
292
+
293
+ @mtimes ||= {}
294
+ files.each do |f|
295
+ full_path = File.join(@project_root, f)
296
+ current_mtime = File.mtime(full_path) rescue next
297
+ if @mtimes[f] && @mtimes[f] != current_mtime
298
+ @events.warn "Perubahan terdeteksi di #{f}. Me-restart..."
299
+ changed = true
300
+ end
301
+ @mtimes[f] = current_mtime
302
+ end
303
+
304
+ if changed
305
+ if @options[:workers] > 0
306
+ @signal_queue << :USR2
307
+ else
308
+ @signal_queue << :TERM
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+
251
315
  def start_control_server
316
+ return if @options[:control_port] == false || @options[:control_port] == nil
252
317
  start_time = Time.now
253
318
  Thread.new do
254
319
  begin
320
+ # Jika port 0, OS akan memilih port acak
255
321
  server = TCPServer.new('127.0.0.1', @options[:control_port])
322
+ actual_port = server.addr[1]
323
+ @events.info "Control Server aktif di http://localhost:#{actual_port}" if @options[:control_port] == 0
324
+
256
325
  loop do
257
326
  client = server.accept
258
327
  uptime = (Time.now - start_time).to_i
259
- stats = { workers: @worker_pids.size, uptime: uptime }.to_json
328
+
329
+ # Hitung memori (RSS) di Linux
330
+ mem = `ps -o rss= -p #{Process.pid}`.strip.to_i rescue 0
331
+
332
+ stats = {
333
+ workers: @worker_pids.size,
334
+ uptime: uptime,
335
+ version: EksaServer::VERSION,
336
+ memory_kb: mem,
337
+ threads: { spawned: @thread_pool&.spawned, waiting: @thread_pool&.waiting }
338
+ }.to_json
260
339
  client.puts "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n#{stats}"
261
340
  client.close
262
341
  end
263
342
  rescue => e
264
- @events.error "Control Server Gagal: #{e.message}"
343
+ @events.error "Control Server Gagal di port #{@options[:control_port]}: #{e.message}"
265
344
  end
266
345
  end
267
346
  end
@@ -273,7 +352,7 @@ class EksaServerCore
273
352
 
274
353
  def print_banner
275
354
  puts "\e[34m"
276
- puts " \e[1mEKSA SERVER v2 \e[0m- \e[90mBy: IshikawaUta\e[0m"
355
+ puts " \e[1mEKSA SERVER v#{EksaServer::VERSION} \e[0m- \e[90mBy: IshikawaUta\e[0m"
277
356
  puts "\e[34m"
278
357
  puts " โ•”โ•โ•—โ•ฆโ•”โ•โ•”โ•โ•—โ•”โ•โ•— โ•”โ•โ•—โ•”โ•โ•—โ•ฆโ•โ•—โ•ฆ โ•ฆโ•”โ•โ•—โ•ฆโ•โ•—"
279
358
  puts " โ•‘โ•ฃ โ• โ•ฉโ•—โ•šโ•โ•—โ• โ•โ•ฃ โ•šโ•โ•—โ•‘โ•ฃ โ• โ•ฆโ•โ•šโ•— โ•”โ•โ•‘โ•ฃ โ• โ•ฆโ•"
@@ -285,7 +364,9 @@ class EksaServerCore
285
364
  def print_server_info
286
365
  puts "\n \e[1mKONFIGURASI SERVER:\e[0m"
287
366
  @binder.listeners.each do |io, type|
288
- puts " \e[36mโ€ข\e[0m Bind [#{type.upcase}]: \e[33m#{io.local_address.inspect_sockaddr}\e[0m"
367
+ # SSLServer tidak punya local_address langsung, panggil to_io
368
+ sock = io.respond_to?(:local_address) ? io : io.to_io
369
+ puts " \e[36mโ€ข\e[0m Bind [#{type.upcase}]: \e[33m#{sock.local_address.inspect_sockaddr}\e[0m"
289
370
  end
290
371
  puts " \e[36mโ€ข\e[0m Threads: \e[33m#{@options[:min_threads]}..#{@options[:max_threads]}\e[0m"
291
372
  puts " \e[36mโ€ข\e[0m Workers: \e[33m#{@options[:workers]} (#{@options[:workers] > 0 ? 'Cluster' : 'Single'} Mode)\e[0m"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eksa-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - IshikawaUta
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: logger
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.6'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.6'
54
68
  description: A modular, concurrent web server built with nio4r.
55
69
  email:
56
70
  - komikers09@gmail.com
@@ -69,6 +83,7 @@ files:
69
83
  - lib/eksa_server/dsl.rb
70
84
  - lib/eksa_server/error.html
71
85
  - lib/eksa_server/events.rb
86
+ - lib/eksa_server/http.rb
72
87
  - lib/eksa_server/thread_pool.rb
73
88
  - lib/eksa_server/version.rb
74
89
  - server.rb