eksa-server 1.0.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 +7 -0
- data/CONTRIBUTING.md +29 -0
- data/LICENSE +21 -0
- data/README.md +71 -0
- data/bin/eksa-server +23 -0
- data/config/eksa_server.rb +11 -0
- data/lib/eksa_server/binder.rb +50 -0
- data/lib/eksa_server/configuration.rb +34 -0
- data/lib/eksa_server/dsl.rb +41 -0
- data/lib/eksa_server/error.html +27 -0
- data/lib/eksa_server/events.rb +52 -0
- data/lib/eksa_server/thread_pool.rb +50 -0
- data/lib/eksa_server/version.rb +4 -0
- data/server.rb +309 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 20f80700434c2365febde215212050d0d859be1d8b67a2225506545b1ec84ded
|
|
4
|
+
data.tar.gz: 0fbf23b72be466483c7cb579f11cf1f4c427d09a361df3199acf67567f3d64ae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fa73c08ba2d35178db74d35e747906e0a9cabe1f8f0019ba3d516c6f71039798cfdd178a7d540b912f66616564f54e081f4f2ce9873c0cd4fb362f87c8a5ceab
|
|
7
|
+
data.tar.gz: 6ba5333277149c9cf839368b244632759417315e9a050f5ca60f39567373d179f4c2d0c80b6ee5e083d8121206e7254b917179db19402af2ea16c080912e4e6f
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Kontribusi ke EksaServer
|
|
2
|
+
|
|
3
|
+
Terima kasih telah tertarik untuk berkontribusi pada EksaServer! Kami sangat menghargai bantuan Anda untuk membuat server ini menjadi lebih baik.
|
|
4
|
+
|
|
5
|
+
## Cara Berkontribusi
|
|
6
|
+
|
|
7
|
+
### 1. Melaporkan Bug
|
|
8
|
+
Jika Anda menemukan masalah, silakan buka *Issue* di repositori GitHub kami dengan menyertakan:
|
|
9
|
+
- Deskripsi bug yang jelas.
|
|
10
|
+
- Langkah-langkah untuk mereproduksi bug.
|
|
11
|
+
- Versi Ruby dan Sistem Operasi yang Anda gunakan.
|
|
12
|
+
|
|
13
|
+
### 2. Kirim Pull Request (PR)
|
|
14
|
+
1. Fork repositori ini.
|
|
15
|
+
2. Buat branch fitur baru (`git checkout -b fitur/nama-fitur`).
|
|
16
|
+
3. Lakukan perubahan kode. Pastikan kode Anda mengikuti gaya penulisan yang ada.
|
|
17
|
+
4. Lakukan commit pada perubahan Anda (`git commit -m 'Menambahkan fitur XYZ'`).
|
|
18
|
+
5. Push ke branch tersebut (`git push origin fitur/nama-fitur`).
|
|
19
|
+
6. Buka Pull Request di GitHub.
|
|
20
|
+
|
|
21
|
+
## Aturan Penulisan Kode
|
|
22
|
+
- Gunakan indentasi 2 spasi.
|
|
23
|
+
- Pastikan semua file di bawah `lib/` memiliki modul/kelas yang terdokumentasi dengan baik.
|
|
24
|
+
- Sertakan komentar jika bagian kode tersebut cukup kompleks.
|
|
25
|
+
|
|
26
|
+
## Pertanyaan?
|
|
27
|
+
Jika ada pertanyaan, jangan ragu untuk menghubungi pengembang utama melalui GitHub Issues.
|
|
28
|
+
|
|
29
|
+
Selamat berkoding! ๐
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IshikawaUta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# EksaServer
|
|
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).
|
|
4
|
+
|
|
5
|
+
## Fitur Utama
|
|
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.
|
|
14
|
+
|
|
15
|
+
## Instalasi
|
|
16
|
+
|
|
17
|
+
Tambahkan baris ini ke Gemfile aplikasi Anda:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'eksa-server'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Lalu jalankan:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Atau instal langsung melalui terminal:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install eksa-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Penggunaan Cepat
|
|
36
|
+
|
|
37
|
+
Cukup jalankan perintah berikut di direktori proyek Anda yang memiliki file `config.ru`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
eksa-server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Anda juga bisa menentukan file rack secara spesifik:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
eksa-server app/config.ru -p 4000
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Konfigurasi
|
|
50
|
+
|
|
51
|
+
Buat file `config/eksa_server.rb` untuk pengaturan tingkat lanjut:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
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"
|
|
58
|
+
|
|
59
|
+
on_worker_boot do |index|
|
|
60
|
+
puts "Worker #{index} siap melayani!"
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Dokumentasi Lengkap
|
|
65
|
+
|
|
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)
|
|
68
|
+
|
|
69
|
+
## Lisensi
|
|
70
|
+
|
|
71
|
+
Proyek ini dirilis di bawah [Lisensi MIT](LICENSE).
|
data/bin/eksa-server
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# bin/eksa-server
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require_relative '../server'
|
|
6
|
+
|
|
7
|
+
options = {}
|
|
8
|
+
OptionParser.new do |opts|
|
|
9
|
+
opts.banner = "Usage: eksa-server [options] [config.ru]"
|
|
10
|
+
|
|
11
|
+
opts.on("-p", "--port PORT", "Port to bind to (default: 3000)") { |v| options[:port] = v.to_i }
|
|
12
|
+
opts.on("-w", "--workers COUNT", "Number of worker processes") { |v| options[:workers] = v.to_i }
|
|
13
|
+
opts.on("-C", "--config PATH", "Path to config file") { |v| options[:config_file] = v }
|
|
14
|
+
end.parse!
|
|
15
|
+
|
|
16
|
+
app_path = ARGV[0] || "config.ru"
|
|
17
|
+
unless File.exist?(app_path)
|
|
18
|
+
puts "\e[31mError: File #{app_path} tidak ditemukan.\e[0m"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
server = EksaServerCore.new(app_path, options)
|
|
23
|
+
server.start
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# lib/eksa_server/binder.rb
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module EksaServer
|
|
7
|
+
class Binder
|
|
8
|
+
attr_reader :listeners
|
|
9
|
+
|
|
10
|
+
def initialize(events)
|
|
11
|
+
@events = events
|
|
12
|
+
@listeners = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_tcp_listener(host, port, options = {})
|
|
16
|
+
@events.info "Mendengarkan di TCP: #{host}:#{port} (SSL: #{!!options[:ssl]})"
|
|
17
|
+
server = TCPServer.new(host, port)
|
|
18
|
+
server.setsockopt(:SOCKET, :REUSEADDR, true)
|
|
19
|
+
|
|
20
|
+
if options[:ssl] && options[:cert] && options[:key]
|
|
21
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
22
|
+
ctx.cert = OpenSSL::X509::Certificate.new(File.read(options[:cert]))
|
|
23
|
+
ctx.key = OpenSSL::PKey::RSA.new(File.read(options[:key]))
|
|
24
|
+
@listeners << [OpenSSL::SSL::SSLServer.new(server, ctx), :tcp_ssl]
|
|
25
|
+
else
|
|
26
|
+
@listeners << [server, :tcp]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_unix_listener(path)
|
|
31
|
+
@events.info "Mendengarkan di Unix Socket: #{path}"
|
|
32
|
+
FileUtils.rm_f(path)
|
|
33
|
+
server = UNIXServer.new(path)
|
|
34
|
+
@listeners << [server, :unix]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@listeners.each do |io, type|
|
|
39
|
+
io.close rescue nil
|
|
40
|
+
if type == :unix
|
|
41
|
+
File.delete(io.path) rescue nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ios
|
|
47
|
+
@listeners.map(&:first)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# lib/eksa_server/configuration.rb
|
|
2
|
+
require_relative 'dsl'
|
|
3
|
+
|
|
4
|
+
module EksaServer
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :options
|
|
7
|
+
|
|
8
|
+
def initialize(user_options = {})
|
|
9
|
+
@options = default_options.merge(user_options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def load_config(path)
|
|
13
|
+
return unless File.exist?(path)
|
|
14
|
+
dsl = DSL.new(@options)
|
|
15
|
+
dsl.instance_eval(File.read(path), path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def default_options
|
|
21
|
+
{
|
|
22
|
+
host: '0.0.0.0',
|
|
23
|
+
port: 3000,
|
|
24
|
+
min_threads: 5,
|
|
25
|
+
max_threads: 16,
|
|
26
|
+
workers: 0,
|
|
27
|
+
control_port: 3001,
|
|
28
|
+
timeout: 15,
|
|
29
|
+
ssl: false,
|
|
30
|
+
binds: []
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# lib/eksa_server/dsl.rb
|
|
2
|
+
module EksaServer
|
|
3
|
+
class DSL
|
|
4
|
+
def initialize(options)
|
|
5
|
+
@options = options
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def threads(min, max)
|
|
9
|
+
@options[:min_threads] = min
|
|
10
|
+
@options[:max_threads] = max
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def workers(count)
|
|
14
|
+
@options[:workers] = count
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bind(url)
|
|
18
|
+
@options[:binds] ||= []
|
|
19
|
+
@options[:binds] << url
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def port(p)
|
|
23
|
+
@options[:port] = p
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def host(h)
|
|
27
|
+
@options[:host] = h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ssl_bind(p, cert, key)
|
|
31
|
+
@options[:ssl] = true
|
|
32
|
+
@options[:port] = p
|
|
33
|
+
@options[:cert] = cert
|
|
34
|
+
@options[:key] = key
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def on_worker_boot(&block)
|
|
38
|
+
@options[:on_worker_boot] = block
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Sistem Error - EksaServer</title>
|
|
5
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
6
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700&display=swap" rel="stylesheet">
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: 'Outfit', sans-serif; background: #020617; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
9
|
+
.glass { background: rgba(30, 41, 59, 0.4); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); padding: 3rem; border-radius: 2rem; border-left: 5px solid #ef4444; max-width: 600px; width: 90%; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="glass shadow-2xl shadow-red-500/10">
|
|
14
|
+
<div class="text-red-500 mb-4">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
17
|
+
</svg>
|
|
18
|
+
</div>
|
|
19
|
+
<h1 class="text-3xl font-bold mb-2">Terjadi Kesalahan Internal</h1>
|
|
20
|
+
<p class="text-slate-400 mb-6">Aplikasi gagal memproses permintaan Anda. Ini mungkin karena kesalahan kode atau template yang hilang.</p>
|
|
21
|
+
<div class="bg-black/40 p-4 rounded-xl font-mono text-sm text-red-300 border border-red-500/20 break-words">
|
|
22
|
+
{{ERROR}}
|
|
23
|
+
</div>
|
|
24
|
+
<p class="mt-6 text-xs text-slate-500 italic uppercase tracking-widest">Mini-Puma Engine Error Protection</p>
|
|
25
|
+
</div>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# lib/eksa_server/events.rb
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
module EksaServer
|
|
5
|
+
class Events
|
|
6
|
+
def initialize(stdout, stderr)
|
|
7
|
+
@stdout = stdout
|
|
8
|
+
@stderr = stderr
|
|
9
|
+
@logger = setup_logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def info(msg)
|
|
13
|
+
@logger.info msg
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def error(msg)
|
|
17
|
+
@logger.error msg
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def warn(msg)
|
|
21
|
+
@logger.warn msg
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def debug(msg)
|
|
25
|
+
@logger.debug msg
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(msg)
|
|
29
|
+
@stdout.puts msg
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def setup_logger
|
|
35
|
+
logger = Logger.new(@stdout)
|
|
36
|
+
logger.formatter = proc do |severity, datetime, _, msg|
|
|
37
|
+
time = datetime.strftime('%H:%M:%S')
|
|
38
|
+
case severity
|
|
39
|
+
when "INFO"
|
|
40
|
+
"\e[34m[#{time}] โน INFO [PID:#{Process.pid}]: #{msg}\e[0m\n"
|
|
41
|
+
when "ERROR"
|
|
42
|
+
"\e[31m[#{time}] โ GAGAL [PID:#{Process.pid}]: \e[1m#{msg}\e[0m\n"
|
|
43
|
+
when "WARN"
|
|
44
|
+
"\e[33m[#{time}] โผ PERINGATAN [PID:#{Process.pid}]: #{msg}\e[0m\n"
|
|
45
|
+
else
|
|
46
|
+
"[#{time}] #{severity}: #{msg}\n"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
logger
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# lib/eksa_server/thread_pool.rb
|
|
2
|
+
require 'thread'
|
|
3
|
+
|
|
4
|
+
module EksaServer
|
|
5
|
+
class ThreadPool
|
|
6
|
+
attr_reader :spawned, :waiting
|
|
7
|
+
|
|
8
|
+
def initialize(min, max, &block)
|
|
9
|
+
@min, @max = min, max
|
|
10
|
+
@block = block
|
|
11
|
+
@todo = Queue.new
|
|
12
|
+
@pool = []
|
|
13
|
+
@spawned = 0
|
|
14
|
+
@waiting = 0
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@min.times { spawn_thread }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <<(work)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
if @waiting == 0 && @spawned < @max
|
|
22
|
+
spawn_thread
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
@todo << work
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shutdown
|
|
29
|
+
@spawned.times { @todo << :exit }
|
|
30
|
+
@pool.each(&:join)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
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
|
|
44
|
+
end
|
|
45
|
+
@mutex.synchronize { @spawned -= 1 }
|
|
46
|
+
end
|
|
47
|
+
@pool << thread
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/server.rb
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# server.rb
|
|
2
|
+
require 'nio'
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'thread'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
require 'rack'
|
|
9
|
+
require 'rackup'
|
|
10
|
+
|
|
11
|
+
# Load Modular Core
|
|
12
|
+
require_relative 'lib/eksa_server/events'
|
|
13
|
+
require_relative 'lib/eksa_server/binder'
|
|
14
|
+
require_relative 'lib/eksa_server/thread_pool'
|
|
15
|
+
require_relative 'lib/eksa_server/configuration'
|
|
16
|
+
|
|
17
|
+
class EksaServerCore
|
|
18
|
+
def initialize(app, config = {})
|
|
19
|
+
@app_path_or_obj = app
|
|
20
|
+
@config = EksaServer::Configuration.new(config)
|
|
21
|
+
|
|
22
|
+
# Load optional config file
|
|
23
|
+
if File.exist?('config/eksa_server.rb')
|
|
24
|
+
@config.load_config('config/eksa_server.rb')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@options = @config.options
|
|
28
|
+
@events = EksaServer::Events.new($stdout, $stderr)
|
|
29
|
+
@binder = EksaServer::Binder.new(@events)
|
|
30
|
+
|
|
31
|
+
@app = load_app(@app_path_or_obj)
|
|
32
|
+
@worker_pids = {}
|
|
33
|
+
@heartbeats = {}
|
|
34
|
+
@hb_dir = "/tmp/eksa_hb"
|
|
35
|
+
FileUtils.mkdir_p(@hb_dir)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def start
|
|
39
|
+
print_banner
|
|
40
|
+
|
|
41
|
+
# Bina Sockets (Binder)
|
|
42
|
+
if @options[:binds].any?
|
|
43
|
+
@options[:binds].each do |url|
|
|
44
|
+
if url =~ %r{tcp://(.*):(\d+)}
|
|
45
|
+
@binder.add_tcp_listener($1, $2.to_i, @options)
|
|
46
|
+
elsif url =~ %r{unix://(.*)}
|
|
47
|
+
@binder.add_unix_listener($1)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
@binder.add_tcp_listener(@options[:host], @options[:port], @options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
print_server_info
|
|
55
|
+
|
|
56
|
+
@events.info "Memuat aplikasi (Optimasi Copy-on-Write)..."
|
|
57
|
+
start_control_server if @options[:control_port]
|
|
58
|
+
|
|
59
|
+
if @options[:workers] > 0
|
|
60
|
+
start_cluster
|
|
61
|
+
else
|
|
62
|
+
run_worker
|
|
63
|
+
end
|
|
64
|
+
rescue Interrupt
|
|
65
|
+
terminate_all
|
|
66
|
+
ensure
|
|
67
|
+
cleanup
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def load_app(app_path_or_obj)
|
|
73
|
+
if app_path_or_obj.is_a?(String) && File.extname(app_path_or_obj) == ".ru"
|
|
74
|
+
app_path = File.expand_path(app_path_or_obj)
|
|
75
|
+
@events.info "Memuat aplikasi dari #{app_path}..."
|
|
76
|
+
app_dir = File.dirname(app_path)
|
|
77
|
+
Dir.chdir(app_dir)
|
|
78
|
+
result = Rack::Builder.parse_file(app_path)
|
|
79
|
+
result.respond_to?(:first) ? result.first : result
|
|
80
|
+
else
|
|
81
|
+
app_path_or_obj
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def start_cluster
|
|
86
|
+
@options[:workers].times { |i| spawn_worker(i) }
|
|
87
|
+
@events.info "Cluster aktif dengan #{@options[:workers]} worker. ๐"
|
|
88
|
+
|
|
89
|
+
trap('INT') { terminate_all }
|
|
90
|
+
trap('TERM') { terminate_all }
|
|
91
|
+
trap('USR2') { phased_restart }
|
|
92
|
+
|
|
93
|
+
loop do
|
|
94
|
+
check_workers
|
|
95
|
+
sleep 2
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def spawn_worker(index)
|
|
100
|
+
pid = fork do
|
|
101
|
+
@options[:on_worker_boot]&.call(index)
|
|
102
|
+
run_worker(index)
|
|
103
|
+
end
|
|
104
|
+
@worker_pids[index] = pid
|
|
105
|
+
@heartbeats[pid] = Time.now
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def check_workers
|
|
109
|
+
@worker_pids.each do |index, pid|
|
|
110
|
+
hb_file = "#{@hb_dir}/#{pid}.hb"
|
|
111
|
+
@heartbeats[pid] = File.mtime(hb_file) if File.exist?(hb_file)
|
|
112
|
+
|
|
113
|
+
if Time.now - @heartbeats[pid] > @options[:timeout]
|
|
114
|
+
@events.warn "Worker #{index} (PID:#{pid}) timeout! Me-restart..."
|
|
115
|
+
Process.kill('KILL', pid) rescue nil
|
|
116
|
+
spawn_worker(index)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def phased_restart
|
|
122
|
+
@events.info "\e[35mMemulai Phased Restart...\e[0m"
|
|
123
|
+
@worker_pids.each do |index, pid|
|
|
124
|
+
Process.kill('TERM', pid)
|
|
125
|
+
Process.wait(pid)
|
|
126
|
+
spawn_worker(index)
|
|
127
|
+
end
|
|
128
|
+
@events.info "Phased Restart selesai. โ"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def terminate_all
|
|
132
|
+
@events.info "Mematikan server..."
|
|
133
|
+
@worker_pids.values.each { |pid| Process.kill('TERM', pid) rescue nil }
|
|
134
|
+
cleanup
|
|
135
|
+
exit
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def run_worker(id = 0)
|
|
139
|
+
srand
|
|
140
|
+
@selector = NIO::Selector.new
|
|
141
|
+
@thread_pool = EksaServer::ThreadPool.new(@options[:min_threads], @options[:max_threads]) do |client|
|
|
142
|
+
process_client(client)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@binder.listeners.each do |io, _|
|
|
146
|
+
@selector.register(io, :r).value = :accept
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
Thread.new do
|
|
150
|
+
loop do
|
|
151
|
+
FileUtils.touch("#{@hb_dir}/#{Process.pid}.hb")
|
|
152
|
+
sleep 5
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
loop do
|
|
157
|
+
@selector.select(5) do |monitor|
|
|
158
|
+
if monitor.value == :accept
|
|
159
|
+
begin
|
|
160
|
+
client = monitor.io.accept_nonblock
|
|
161
|
+
@selector.register(client, :r).value = :read
|
|
162
|
+
rescue IO::WaitReadable
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
client = monitor.io
|
|
166
|
+
@selector.deregister(client)
|
|
167
|
+
@thread_pool << client
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
rescue Interrupt, SignalException
|
|
172
|
+
ensure
|
|
173
|
+
@thread_pool&.shutdown
|
|
174
|
+
@selector&.close
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
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 }
|
|
208
|
+
|
|
209
|
+
# Panggil aplikasi Rack
|
|
210
|
+
begin
|
|
211
|
+
status, res_headers, res_body = @app.call(env)
|
|
212
|
+
rescue => e
|
|
213
|
+
@events.error "Aplikasi gagal merespons: #{e.message}"
|
|
214
|
+
status = 500
|
|
215
|
+
res_headers = { "Content-Type" => "text/html" }
|
|
216
|
+
res_body = [render_error_page(e)]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Send Response
|
|
220
|
+
response = "HTTP/1.1 #{status} OK\r\n"
|
|
221
|
+
res_headers.each { |k, v| response << "#{k}: #{v}\r\n" }
|
|
222
|
+
|
|
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})"
|
|
233
|
+
client.close
|
|
234
|
+
rescue => e
|
|
235
|
+
@events.error "Kesalahan tingkat rendah: #{e.message}"
|
|
236
|
+
client.close rescue nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def render_error_page(e)
|
|
240
|
+
# Gunakan path absolut relatif terhadap file ini agar aman saat jadi gem
|
|
241
|
+
template_path = File.expand_path('../lib/eksa_server/error.html', __FILE__)
|
|
242
|
+
@error_template ||= File.read(template_path) rescue nil
|
|
243
|
+
|
|
244
|
+
if @error_template
|
|
245
|
+
@error_template.gsub("{{ERROR}}", "#{e.class}: #{e.message}")
|
|
246
|
+
else
|
|
247
|
+
"<html><body><h1>Internal Server Error</h1><p>#{e.message}</p></body></html>"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def start_control_server
|
|
252
|
+
start_time = Time.now
|
|
253
|
+
Thread.new do
|
|
254
|
+
begin
|
|
255
|
+
server = TCPServer.new('127.0.0.1', @options[:control_port])
|
|
256
|
+
loop do
|
|
257
|
+
client = server.accept
|
|
258
|
+
uptime = (Time.now - start_time).to_i
|
|
259
|
+
stats = { workers: @worker_pids.size, uptime: uptime }.to_json
|
|
260
|
+
client.puts "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n#{stats}"
|
|
261
|
+
client.close
|
|
262
|
+
end
|
|
263
|
+
rescue => e
|
|
264
|
+
@events.error "Control Server Gagal: #{e.message}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def cleanup
|
|
270
|
+
@binder&.close
|
|
271
|
+
FileUtils.rm_rf(@hb_dir)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def print_banner
|
|
275
|
+
puts "\e[34m"
|
|
276
|
+
puts " \e[1mEKSA SERVER v2 \e[0m- \e[90mBy: IshikawaUta\e[0m"
|
|
277
|
+
puts "\e[34m"
|
|
278
|
+
puts " โโโโฆโโโโโโโโ โโโโโโโฆโโโฆ โฆโโโโฆโโ"
|
|
279
|
+
puts " โโฃ โ โฉโโโโโ โโฃ โโโโโฃ โ โฆโโโ โโโโฃ โ โฆโ"
|
|
280
|
+
puts " โโโโฉ โฉโโโโฉ โฉ โโโโโโโฉโโ โโโ โโโโฉโโ"
|
|
281
|
+
puts "\e[0m"
|
|
282
|
+
puts " \e[90mPowered by nio4r | Modular High-Performance Engine\e[0m"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def print_server_info
|
|
286
|
+
puts "\n \e[1mKONFIGURASI SERVER:\e[0m"
|
|
287
|
+
@binder.listeners.each do |io, type|
|
|
288
|
+
puts " \e[36mโข\e[0m Bind [#{type.upcase}]: \e[33m#{io.local_address.inspect_sockaddr}\e[0m"
|
|
289
|
+
end
|
|
290
|
+
puts " \e[36mโข\e[0m Threads: \e[33m#{@options[:min_threads]}..#{@options[:max_threads]}\e[0m"
|
|
291
|
+
puts " \e[36mโข\e[0m Workers: \e[33m#{@options[:workers]} (#{@options[:workers] > 0 ? 'Cluster' : 'Single'} Mode)\e[0m"
|
|
292
|
+
puts " \e[36mโข\e[0m Control: \e[33mlocalhost:#{@options[:control_port]}\e[0m\n\n"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
if __FILE__ == $0
|
|
297
|
+
# Default ke config.ru di direktori saat ini
|
|
298
|
+
app_path = ARGV[0] || "config.ru"
|
|
299
|
+
|
|
300
|
+
app = if File.exist?(app_path)
|
|
301
|
+
app_path
|
|
302
|
+
else
|
|
303
|
+
proc { |env|
|
|
304
|
+
[404, {"Content-Type" => "text/html"}, ["<h1>Aplikasi tidak ditemukan di #{Dir.pwd}/#{app_path}</h1><p>Silakan buat file config.ru atau masukkan path sebagai argumen.</p>"]]
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
EksaServerCore.new(app).start
|
|
309
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eksa-server
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- IshikawaUta
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: nio4r
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rackup
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
description: A modular, concurrent web server built with nio4r.
|
|
55
|
+
email:
|
|
56
|
+
- komikers09@gmail.com
|
|
57
|
+
executables:
|
|
58
|
+
- eksa-server
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- CONTRIBUTING.md
|
|
63
|
+
- LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- bin/eksa-server
|
|
66
|
+
- config/eksa_server.rb
|
|
67
|
+
- lib/eksa_server/binder.rb
|
|
68
|
+
- lib/eksa_server/configuration.rb
|
|
69
|
+
- lib/eksa_server/dsl.rb
|
|
70
|
+
- lib/eksa_server/error.html
|
|
71
|
+
- lib/eksa_server/events.rb
|
|
72
|
+
- lib/eksa_server/thread_pool.rb
|
|
73
|
+
- lib/eksa_server/version.rb
|
|
74
|
+
- server.rb
|
|
75
|
+
homepage: https://github.com/IshikawaUta/eksa-server
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/IshikawaUta/eksa-server
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 2.6.0
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 3.6.7
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: High Performance Asynchronous Ruby Web Server
|
|
97
|
+
test_files: []
|