eksa-server 1.0.0 โ 1.1.1
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/README.md +51 -28
- data/bin/eksa-server +14 -0
- data/lib/eksa_server/configuration.rb +18 -2
- data/lib/eksa_server/events.rb +5 -0
- data/lib/eksa_server/http.rb +78 -0
- data/lib/eksa_server/thread_pool.rb +12 -10
- data/lib/eksa_server/version.rb +1 -1
- data/server.rb +139 -58
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84678e85d0e71d308513b9e9a6b822571d6099648b696232581113bf72b5241f
|
|
4
|
+
data.tar.gz: 9036ae04dca93f53f5946e73f1f981c9b781eb60f5fce97d14ffea84a6140faa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df58caa941d6784b31ed7786e38838fa0966dc77e49046ed2b942db011718342dd319a6b868cc7e51d560180914fd870379532ca705e9c841749478f34f8c733
|
|
7
|
+
data.tar.gz: 8824634c7c6821cfe477bd08c6d30d2fa44a5e00e6dd574851d2198290e56f1b57de464c59691610d59c2bf36d57479b37c90008b62d7340b1d4629bd99c4644
|
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
|
|
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**:
|
|
8
|
-
- ๐งต **
|
|
9
|
-
- ๐๏ธ **Cluster Mode**:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
|
18
|
+
Tambahkan ke Gemfile Anda:
|
|
18
19
|
|
|
19
20
|
```ruby
|
|
20
21
|
gem 'eksa-server'
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
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
|
-
|
|
32
|
+
Jalankan di direktori aplikasi Rack Anda:
|
|
38
33
|
|
|
39
34
|
```bash
|
|
40
35
|
eksa-server
|
|
41
36
|
```
|
|
42
37
|
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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 #
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
73
|
+
puts "Worker #{index} siap beraksi!"
|
|
61
74
|
end
|
|
62
75
|
```
|
|
63
76
|
|
|
64
|
-
##
|
|
77
|
+
## Statistik (Control Server)
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
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.1",
|
|
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,
|
data/lib/eksa_server/events.rb
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
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.errors' => $stderr,
|
|
39
|
+
'rack.multithread' => true,
|
|
40
|
+
'rack.multiprocess' => @options[:workers] > 0,
|
|
41
|
+
'rack.run_once' => false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
headers.each do |k, v|
|
|
45
|
+
key = k.upcase.gsub('-', '_')
|
|
46
|
+
if key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH'
|
|
47
|
+
env[key] = v
|
|
48
|
+
else
|
|
49
|
+
env["HTTP_#{key}"] = v
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Read body if Content-Length is present
|
|
54
|
+
content_length = env['CONTENT_LENGTH'].to_i
|
|
55
|
+
body = content_length > 0 ? @client.read(content_length) : ""
|
|
56
|
+
env['rack.input'] = StringIO.new(body)
|
|
57
|
+
|
|
58
|
+
env
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Response
|
|
63
|
+
def self.send_response(client, status, headers, body)
|
|
64
|
+
response = "HTTP/1.1 #{status} OK\r\n"
|
|
65
|
+
headers.each { |k, v| response << "#{k}: #{v}\r\n" }
|
|
66
|
+
|
|
67
|
+
full_body = ""
|
|
68
|
+
body = [body] unless body.respond_to?(:each)
|
|
69
|
+
body.each { |chunk| full_body << chunk }
|
|
70
|
+
body.close if body.respond_to?(:close)
|
|
71
|
+
|
|
72
|
+
response << "Content-Length: #{full_body.bytesize}\r\n\r\n"
|
|
73
|
+
response << full_body
|
|
74
|
+
|
|
75
|
+
client.write(response)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -33,18 +33,20 @@ module EksaServer
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def spawn_thread
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
@
|
|
48
|
+
@pool << thread
|
|
46
49
|
end
|
|
47
|
-
@pool << thread
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
end
|
data/lib/eksa_server/version.rb
CHANGED
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,
|
|
19
|
-
@app_path_or_obj = app
|
|
20
|
-
@config = EksaServer::Configuration.new
|
|
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') {
|
|
90
|
-
trap('TERM') {
|
|
91
|
-
trap('USR2') {
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
+
version: 1.1.1
|
|
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
|