Module: Rakit::StaticWebServer
- Defined in:
- lib/rakit/static_web_server.rb
Overview
Publish static sites into a configurable root, regenerate a root index, and control a local HTTP server (start/stop/running). CLI: rakit static-web-server (start|stop|running|publish). See contracts/ruby-api.md and quickstart in specs/003-static-web-server/quickstart.md.
Constant Summary collapse
- SITE_NAME_REGEX =
/\A[a-z0-9\-]+\z/.freeze
- STOP_TIMEOUT =
5
Class Attribute Summary collapse
-
.port ⇒ Integer
Default port for start (default 5099).
-
.root ⇒ String
Static root directory (default ~/.rakit/www_root).
Class Method Summary collapse
-
.ensure_root ⇒ Object
T008: Create root directory if missing; used before publish and start.
-
.pid_file_path ⇒ Object
T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
-
.publish(site_name, source_directory) ⇒ true
Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
-
.regenerate_root_index ⇒ Object
T017: Write root/index.html listing all site subdirectories alphabetically with links.
-
.running? ⇒ Boolean
Whether the server process is currently running (PID file + process check).
-
.server_binary ⇒ Object
T007: Server binary (miniserve) on PATH; used by start.
- .server_binary=(path) ⇒ Object
-
.start(options = {}) ⇒ true
Start background server serving root.
-
.stop ⇒ true
Gracefully terminate server process; remove PID file.
-
.validate_site_name!(site_name) ⇒ Object
T005: Validate site name; raise ArgumentError before any filesystem write.
Class Attribute Details
.port ⇒ Integer
Returns Default port for start (default 5099). Used when no override passed to start.
16 17 18 |
# File 'lib/rakit/static_web_server.rb', line 16 def port @port end |
.root ⇒ String
Returns Static root directory (default ~/.rakit/www_root). Used by publish and server lifecycle.
14 15 16 |
# File 'lib/rakit/static_web_server.rb', line 14 def root @root end |
Class Method Details
.ensure_root ⇒ Object
T008: Create root directory if missing; used before publish and start.
47 48 49 |
# File 'lib/rakit/static_web_server.rb', line 47 def self.ensure_root FileUtils.mkdir_p(root) end |
.pid_file_path ⇒ Object
T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
29 30 31 |
# File 'lib/rakit/static_web_server.rb', line 29 def self.pid_file_path ::File.("~/.rakit/static_web_server.pid") end |
.publish(site_name, source_directory) ⇒ true
Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/rakit/static_web_server.rb', line 56 def self.publish(site_name, source_directory) validate_site_name!(site_name) src = ::File.(source_directory) raise ArgumentError, "source_directory does not exist: #{source_directory}" unless ::File.exist?(src) raise ArgumentError, "source_directory is not a directory: #{source_directory}" unless ::File.directory?(src) ensure_root # Check root is writable before we do any copy (T015 / edge case). unless ::File.writable?(root) raise ArgumentError, "root directory is not writable: #{root}" end target = ::File.join(root, site_name) temp = ::File.join(root, ".tmp_#{site_name}_#{Process.pid}_#{rand(1_000_000)}") FileUtils.mkdir_p(temp) FileUtils.cp_r(::File.join(src, "."), temp) FileUtils.rm_rf(target) FileUtils.mv(temp, target) regenerate_root_index true end |
.regenerate_root_index ⇒ Object
T017: Write root/index.html listing all site subdirectories alphabetically with links.
149 150 151 152 153 154 155 156 157 |
# File 'lib/rakit/static_web_server.rb', line 149 def self.regenerate_root_index entries = Dir.entries(root).sort.select do |e| e != "." && e != ".." && e != "index.html" && !e.start_with?(".") && ::File.directory?(::File.join(root, e)) end html = ["<!DOCTYPE html>", "<html><head><meta charset=\"utf-8\"><title>Published sites</title></head><body>", "<h1>Published sites</h1>", "<ul>"] entries.each { |name| html << "<li><a href=\"/#{name}/\">#{name}</a></li>" } html << "</ul></body></html>" ::File.write(::File.join(root, "index.html"), html.join("\n")) end |
.running? ⇒ Boolean
Whether the server process is currently running (PID file + process check). No side effects.
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rakit/static_web_server.rb', line 80 def self.running? path = pid_file_path return false unless ::File.file?(path) pid = ::File.read(path).strip.to_i return false if pid <= 0 Process.getpgid(pid) true rescue Errno::ESRCH, Errno::EPERM false end |
.server_binary ⇒ Object
T007: Server binary (miniserve) on PATH; used by start. Returns path or nil.
34 35 36 37 38 39 40 |
# File 'lib/rakit/static_web_server.rb', line 34 def self.server_binary @server_binary ||= begin path = ENV["RAKIT_STATIC_SERVER_BINARY"] return path if path && !path.empty? which("miniserve") end end |
.server_binary=(path) ⇒ Object
42 43 44 |
# File 'lib/rakit/static_web_server.rb', line 42 def self.server_binary=(path) @server_binary = path end |
.start(options = {}) ⇒ true
Start background server serving root. Idempotent: if already running, no-op.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/rakit/static_web_server.rb', line 95 def self.start( = {}) return true if running? ensure_root p = [:port] || port raise "port #{p} is already in use; change port or stop the other process" if port_in_use?(p) bin = server_binary raise "static server binary (miniserve) not found on PATH; install miniserve or set RAKIT_STATIC_SERVER_BINARY" unless bin root_path = ::File.(root) pid = spawn(bin, root_path, "--port", p.to_s, out: ::File::NULL, err: ::File::NULL) Process.detach(pid) FileUtils.mkdir_p(::File.dirname(pid_file_path)) ::File.write(pid_file_path, pid.to_s) true end |
.stop ⇒ true
Gracefully terminate server process; remove PID file. Fail-fast if not running.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/rakit/static_web_server.rb', line 115 def self.stop path = pid_file_path raise "server is not running (no PID file at #{path})" unless ::File.file?(path) pid = ::File.read(path).strip.to_i raise "server is not running (invalid PID in #{path})" if pid <= 0 begin Process.getpgid(pid) rescue Errno::ESRCH ::File.delete(path) raise "server is not running (process #{pid} not found)" end Process.kill(:TERM, pid) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STOP_TIMEOUT while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline begin Process.wait(pid, Process::WNOHANG) break rescue Errno::ECHILD break end sleep 0.1 end begin Process.getpgid(pid) Process.kill(:KILL, pid) Process.wait(pid) rescue Errno::ESRCH, Errno::ECHILD # already gone end ::File.delete(path) if ::File.file?(path) true end |
.validate_site_name!(site_name) ⇒ Object
T005: Validate site name; raise ArgumentError before any filesystem write.
23 24 25 26 |
# File 'lib/rakit/static_web_server.rb', line 23 def self.validate_site_name!(site_name) return if site_name.is_a?(String) && site_name.match?(SITE_NAME_REGEX) raise ArgumentError, "site_name must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{site_name.inspect}" end |