Skip to content

Commit

Permalink
Use INotify to update the cache if files are changed
Browse files Browse the repository at this point in the history
  • Loading branch information
dscottboggs committed Feb 17, 2020
1 parent bd2bc69 commit 4c6102b
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 82 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/bin/
/.shards/
*.dwarf
.vscode
6 changes: 6 additions & 0 deletions shard.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 1.0
shards:
inotify:
github: petoem/inotify.cr
version: 1.0.1

5 changes: 5 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ targets:
crystal: 0.32.1

license: MIT

dependencies:
inotify:
github: petoem/inotify.cr
version: 1.0.1
159 changes: 105 additions & 54 deletions src/cached_static_server.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
require "./log"
require "inotify"
require "./manual_memory_management"
require "./cli"

module CachedStaticServer
TEMPLATE_USAGE = "\
Expand All @@ -8,6 +11,7 @@ module CachedStaticServer

class Server
include Log
include ManualMemoryManagement

def self.default_not_found_action(request : HTTP::Request) : String
"No file found at #{request.path}"
Expand All @@ -26,21 +30,110 @@ module CachedStaticServer
end

private def read_files_into_cache(dir : Path = @parent_dir)
debug "scanning dir #{dir}"
log "scanning dir %s", dir.to_s
raise "tried to scan dir but was file at #{dir}" unless File.directory? path: dir
watch_dir path: dir
Dir.each_child dir.to_s do |file|
fullpath = dir / file
next read_files_into_cache fullpath if File.directory? fullpath
debug "caching file #{fullpath}"
File.open fullpath do |file|
size = file.info.size
buf = Bytes.new LibC.malloc(size).as(Pointer(UInt8)), size
# uninitialized, forever-living (no GC) pointer.
# these need to be kept around for the life of the
# program anyway, and we're about to fill
# in the buffer, so why bother zeroing it?
read_count = file.read buf
raise "read #{read_count} bytes from file #{fullpath} of size #{size}" if read_count != size
cache[fullpath] = buf
read_in_one_file fullpath
end
end

private def read_in_one_file(path : Path)
log "caching file %s", path.to_s
File.open path do |file|
debug "file opened at ", path.to_s
size = file.info.size
buf = malloc size, path
debug "buffer allocated"
read_count = file.read buf
raise "read #{read_count} bytes from file #{path} of size #{size}" if read_count != size
debug "file read successfully"
cache[path] = buf
watch path
rescue err
LibC.free buf if buf
raise err
end
end

def watch(path : Path)
debug "watching file %s", path.to_s
Inotify.watch path.to_s do |event|
debug "processing file event at %s", path.to_s
case event.type
when Inotify::Event::Type::DELETE, Inotify::Event::Type::DELETE_SELF
log "delete event for %s received.", path.to_s
if buf = cache.delete path
debug "freeing buffer at 0x%X", buf.to_unsafe.address
LibC.free buf
end
when Inotify::Event::Type::MODIFY
log "modify event for %s received.", path.to_s
update_file_at path
end
end
end

def watch_dir(path : Path)
debug "watching directory #{path}"
Inotify.watch path.to_s do |event|
debug "processing directory event at %s", path.to_s
evpath = event.path.try { |p| Path.new p } || path
if name = event.name
evpath /= name
end
debug "event path was #{evpath} and type was #{event.type}"
case event.type
when Inotify::Event::Type::CREATE
if File.file? evpath
log "caching new file at #{evpath}"
update_file_at evpath
elsif File.directory? evpath
log "caching new directory at #{evpath}"
read_files_into_cache evpath
end
when Inotify::Event::Type::DELETE
if buf = cache.delete evpath
debug "freeing buffer for #{evpath} at #{addr of: buf}"
LibC.free buf
end
when Inotify::Event::Type::DELETE_SELF
# delete any file under this path
cache.reject! do |cache_path, buf|
next unless cache_path > path
log "clearing deleted file under #{path} at #{cache_path} from cache"
LibC.free buf
true
end
next
else pp! event.type
end
end
end

def update_file_at(path)
debug "file at #{path} was updated"
File.open path do |file|
debug "opened file at #{path}"
size = file.info.size
if buf = cache.delete path
debug "overwriting existing buffer at #{addr of: buf}"
if buf.size == size
debug "same size, no need to reallocate"
file.read buf
else
debug "reallocating buffer for file at #{path} from #{buf.size} to #{size} at #{addr of: buf}"
buf = realloc buf, size
debug "buffer is now at #{addr of: buf} through 0x#{(buf.to_unsafe.address + size).to_s base: 16}"
file.read buf
end
cache[path] = buf
log "updated cache for file at #{path}"
else
debug "file at #{path} not found in cache, reading in fresh."
read_in_one_file path
end
end
end
Expand All @@ -59,46 +152,4 @@ module CachedStaticServer
@not_found_action = action
end
end

class CLI
def self.default_port : UInt16
(ENV["port"]? || 12345).to_u16
end

def self.build_server(args = ARGV)
port, parent, addr = default_port, Dir.current, "0.0.0.0"
template = nil
OptionParser.parse args do |parser|
parser.banner = "Cached Static File Server"
parser.on "-h", "--help", "Show this help" do
puts parser
exit 0
end
parser.on "-p PORT",
"--port PORT",
"bind the server to a port. Defaults to 12345" do |p|
port = p.to_u16
end
parser.on "-d DIR",
"--parent DIR",
"specify the parent directory under which files will be served." do |dir|
parent = dir
end
parser.on "-a ADDR", "--address ADDR", "bind to a given interface address" do |a|
addr = a
end
# parser.on "-t TEMPLATE", "--on-not-found TEMPLATE", TEMPLATE_USAGE do |file|
# template = file
# end
end
server = Server.new parent, port, addr
# if template
# server.on_not_found do |request|
# ECR.render template
# end
# end
pp! parent, port, addr
server
end
end
end
42 changes: 42 additions & 0 deletions src/cli.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class CachedStaticServer::CLI
def self.default_port : UInt16
(ENV["port"]? || 12345).to_u16
end

def self.build_server(args = ARGV)
port, parent, addr = default_port, Dir.current, "0.0.0.0"
template = nil
OptionParser.parse args do |parser|
parser.banner = "Cached Static File Server"
parser.on "-h", "--help", "Show this help" do
puts parser
exit 0
end
parser.on "-p PORT",
"--port PORT",
"bind the server to a port. Defaults to 12345" do |p|
port = p.to_u16
end
parser.on "-d DIR",
"--parent DIR",
"specify the parent directory under which files will be served." do |dir|
parent = dir
end
parser.on "-a ADDR", "--address ADDR", "bind to a given interface address" do |a|
addr = a
end
# parser.on "-t TEMPLATE", "--on-not-found TEMPLATE", TEMPLATE_USAGE do |file|
# template = file
# end
end
server = Server.new parent, port, addr
# if template
# server.on_not_found do |request|
# ECR.render template
# end
# end
pp! parent, port, addr
server
end
end

48 changes: 20 additions & 28 deletions src/log.cr
Original file line number Diff line number Diff line change
@@ -1,24 +1,4 @@
module Log
# use VERBOSE=yes to enable verbose logging
def verbose?
!ENV["VERBOSE"]?.nil?
end

# use DEBUG=yes to enable debug messages
def debug?
!ENV["DEBUG"]?.nil?
end

# use WARNINGS=none to disable warnings
def no_warnings?
ENV["WARNINGS"]?.nil?
end

# use ERRORS=no to disable errors
def no_errors?
ENV["ERRORS"]?.nil?
end

private def output(msg, *fmt, to log : IO)
if fmt.empty?
log.puts msg
Expand All @@ -27,23 +7,35 @@ module Log
end
end

# use VERBOSE=yes to enable verbose logging
def log(msg, *fmt)
return unless verbose?
output msg, *fmt, to: STDOUT
{% if env "VERBOSE" %}
output msg, *fmt, to: STDOUT
{% end %}
end

# use DEBUG=yes to enable debug messages
def debug(msg, *fmt)
return unless debug?
output msg, *fmt, to: STDOUT
{% if env "DEBUG" %}
output msg, *fmt, to: STDOUT
{% end %}
end

# use WARNINGS=none to disable warnings
def warn(msg, *fmt)
return if no_warnings?
output msg, *fmt, to: STDERR
{% if env("WARNINGS") == "none" %}
output msg, *fmt, to: STDERR
{% end %}
end

# use ERRORS=no to disable errors
def error(msg, *fmt)
return if no_errors?
output msg, *fmt, to: STDERR
{% if env("ERRORS") == "no" %}
output msg, *fmt, to: STDERR
{% end %}
end

def addr(of buf)
"0x" + buf.to_unsafe.address.to_s base: 16
end
end
30 changes: 30 additions & 0 deletions src/manual_memory_management.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "./log"

module ManualMemoryManagement
include Log

def malloc(size, file) : Bytes
debug "allocating #{size} bytes for #{file}"
ptr = LibC.malloc(size).as Pointer(UInt8)
if ptr.null?
raise Errno.new "\
failed to allocate 0x#{size.to_s base: 16} bytes for #{file}"
end
debug "successfully returning #{size} allocated bytes at #{ptr.address.to_s base: 16}"
Bytes.new pointer: ptr, size: size
end

def realloc(buf, size) : Bytes
old_size = buf.size
debug "reallocating buffer from #{old_size} to #{size} bytes at #{addr of: buf}"
ptr = LibC.realloc(buf, size).as Pointer(UInt8)
if ptr.null?
raise Errno.new "\
failed to reallocate buffer from 0x#{old_size.to_s base: 16} bytes \
to 0x#{size.to_s base: 16} bytes"
end

debug "successfully returning #{size} allocated bytes at #{ptr.address.to_s base: 16}"
Bytes.new pointer: ptr, size: size
end
end

0 comments on commit 4c6102b

Please sign in to comment.