Module: Rakit::StaticWebServer
- Defined in:
- lib/rakit/static_web_server.rb
Overview
Publish static sites into a configurable root (~/.rakit/wwwroot), regenerate a root index, and control a local HTTP server (start/stop/running). Host and port are configurable (host default 127.0.0.1, port 5099) and returned regardless of server state for URL building. CLI: rakit static-web-server (start|stop|running|publish|view). See specs/003-static-web-server/contracts/ruby-api.md and specs/007-wwwroot-docs-hugo-view/contracts/ruby-api.md.
Constant Summary collapse
- SITE_NAME_REGEX =
/\A[a-z0-9\-]+\z/.freeze
- STOP_TIMEOUT =
5
Class Attribute Summary collapse
-
.host ⇒ String
Host for building URLs (default 127.0.0.1).
-
.port ⇒ Integer
Default port for start (default 5099).
-
.root ⇒ String
Static root directory (default ~/.rakit/wwwroot).
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.
- .validate_site_path!(site_name) ⇒ Object
-
.view(relative_path) ⇒ true
Open a path on the static server in the default browser.
Class Attribute Details
.host ⇒ String
17 18 19 |
# File 'lib/rakit/static_web_server.rb', line 17 def host @host end |
.port ⇒ Integer
19 20 21 |
# File 'lib/rakit/static_web_server.rb', line 19 def port @port end |
.root ⇒ String
15 16 17 |
# File 'lib/rakit/static_web_server.rb', line 15 def root @root end |
Class Method Details
.ensure_root ⇒ Object
T008: Create root directory if missing; used before publish and start.
61 62 63 |
# File 'lib/rakit/static_web_server.rb', line 61 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).
43 44 45 |
# File 'lib/rakit/static_web_server.rb', line 43 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.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/rakit/static_web_server.rb', line 70 def self.publish(site_name, source_directory) validate_site_path!(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 unless ::File.writable?(root) raise ArgumentError, "root directory is not writable: #{root}" end target = ::File.join(root, site_name) temp_name = ".tmp_#{site_name.to_s.gsub(::File::SEPARATOR, '_')}_#{Process.pid}_#{rand(1_000_000)}" temp = ::File.join(root, temp_name) FileUtils.mkdir_p(temp) FileUtils.cp_r(::File.join(src, "."), temp) FileUtils.mkdir_p(::File.dirname(target)) 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.
178 179 180 181 182 183 184 185 186 |
# File 'lib/rakit/static_web_server.rb', line 178 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.
95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rakit/static_web_server.rb', line 95 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.
48 49 50 51 52 53 54 |
# File 'lib/rakit/static_web_server.rb', line 48 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
56 57 58 |
# File 'lib/rakit/static_web_server.rb', line 56 def self.server_binary=(path) @server_binary = path end |
.start(options = {}) ⇒ true
Start background server serving root. Idempotent: if already running, no-op.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/rakit/static_web_server.rb', line 110 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.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/rakit/static_web_server.rb', line 130 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. Single segment (e.g. “mysite”) or multi-segment path (e.g. “louparslow/rakit”); each segment must match SITE_NAME_REGEX.
28 29 30 |
# File 'lib/rakit/static_web_server.rb', line 28 def self.validate_site_name!(site_name) validate_site_path!(site_name) end |
.validate_site_path!(site_name) ⇒ Object
32 33 34 35 36 37 38 39 40 |
# File 'lib/rakit/static_web_server.rb', line 32 def self.validate_site_path!(site_name) return if site_name.nil? || site_name.to_s.empty? site_name.to_s.split(::File::SEPARATOR).each do |seg| next if seg.empty? unless seg.match?(SITE_NAME_REGEX) raise ArgumentError, "site_name segment must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{seg.inspect}" end end end |
.view(relative_path) ⇒ true
Open a path on the static server in the default browser. Ensures server is running (starts if not), builds URL, launches browser.
167 168 169 170 171 172 173 174 175 |
# File 'lib/rakit/static_web_server.rb', line 167 def self.view(relative_path) start unless running? path = relative_path.to_s.strip path = "/#{path}" unless path.empty? || path.start_with?("/") path = "/" if path.empty? url = "http://#{host}:#{port}#{path}" launch_browser(url) true end |