require 'websocket' require 'socket' require 'fiber' require 'listen' require 'optparse' require 'json' module Hyperstack class Hotloader # Most of this lifted from https://github.com/saward/Rubame class Server attr_reader :directories def initialize(options) Socket.do_not_reverse_lookup @hostname = '0.0.0.0' @port = options[:port] setup_directories(options) @reading = [] @writing = [] @clients = {} # Socket as key, and Client as value @socket = TCPServer.new(@hostname, @port) @reading.push @socket end # adds known directories automatically if they exist # - rails js app/assets/javascripts app/assets/stylesheets # - reactrb rails defaults app/views/components # - you tell me and I'll add them def setup_directories(options) @directories = options[:directories] || [] [ 'app/assets/javascripts', 'app/assets/stylesheets', 'app/views/components' ].each { |known_dir| if !@directories.include?(known_dir) && File.exists?(known_dir) @directories << known_dir end } end def accept socket = @socket.accept_nonblock @reading.push socket handshake = WebSocket::Handshake::Server.new client = Client.new(socket, handshake, self) while line = socket.gets client.handshake << line break if client.handshake.finished? end if client.handshake.valid? @clients[socket] = client client.write handshake.to_s client.opened = true return client else close(client) end return nil end def send_updated_file(modified_file) if modified_file =~ /\.rb(\.erb)?$/ if File.exist?(modified_file) file_contents = File.read(modified_file).force_encoding(Encoding::UTF_8) end relative_path = Pathname.new(modified_file).relative_path_from(Pathname.new(Dir.pwd)).to_s asset_path = nil @directories.each do |directory| next unless relative_path =~ /^#{directory}/ asset_path = relative_path.gsub("#{directory}/", '') end update = { type: 'ruby', filename: modified_file, asset_path: asset_path, source_code: file_contents || '' }.to_json end if modified_file =~ /\.s?[ac]ss$/ && File.exist?(modified_file) # Don't know how to remove css definitions at the moment # TODO: Switch from hard-wired path assumptions to using SASS/sprockets config relative_path = Pathname.new(modified_file).relative_path_from(Pathname.new(Dir.pwd)) url = relative_path.to_s .sub('public/','') .sub('/sass/','/') .sub(/\.s[ac]ss/, '.css') update = { type: 'css', filename: modified_file, url: url }.to_json end if update @clients.each { |socket, client| client.send(update) } end end PROGRAM = 'opal-hot-reloader' def loop listener = Listen.to(*@directories, only: %r{\.(rb(\.erb)?|s?[ac]ss)$}) do |modified, added, removed| (removed + modified + added).each { |file| send_updated_file(file) } puts "removed absolute path: #{removed}" puts "modified absolute path: #{modified}" puts "added absolute path: #{added}" end listener.start puts "#{PROGRAM}: starting..." while (!$quit) run do |client| client.onopen do puts "#{PROGRAM}: client open" end client.onmessage do |mess| puts "PROGRAM: message received: #{mess}" unless mess == '' end client.onclose do puts "#{PROGRAM}: client closed" end end sleep 0.2 end end def read(client) pairs = client.socket.recvfrom(2000) messages = [] if pairs[0].length == 0 close(client) else client.frame << pairs[0] while f = client.frame.next if (f.type == :close) close(client) return messages else messages.push f end end end return messages end def close(client) @reading.delete client.socket @clients.delete client.socket begin client.socket.close rescue end client.closed = true end def run(time = 0, &blk) readable, writable = IO.select(@reading, @writing, nil, 0) if readable readable.each do |socket| client = @clients[socket] if socket == @socket client = accept else msg = read(client) client.messaged = msg end blk.call(client) if client and blk end end # Check for lazy send items timer_start = Time.now time_passed = 0 begin @clients.each do |s, c| c.send_some_lazy(5) end time_passed = Time.now - timer_start end while time_passed < time end def stop @socket.close end end class Client attr_accessor :socket, :handshake, :frame, :opened, :messaged, :closed def initialize(socket, handshake, server) @socket = socket @handshake = handshake @frame = WebSocket::Frame::Incoming::Server.new(:version => @handshake.version) @opened = false @messaged = [] @lazy_queue = [] @lazy_current_queue = nil @closed = false @server = server end def write(data) @socket.write data end def send(data) frame = WebSocket::Frame::Outgoing::Server.new(:version => @handshake.version, :data => data, :type => :text) begin @socket.write frame @socket.flush rescue @server.close(self) unless @closed end end def lazy_send(data) @lazy_queue.push data end def get_lazy_fiber # Create the fiber if needed if @lazy_fiber == nil or !@lazy_fiber.alive? @lazy_fiber = Fiber.new do @lazy_current_queue.each do |data| send(data) Fiber.yield unless @lazy_current_queue[-1] == data end end end return @lazy_fiber end def send_some_lazy(count) # To save on cpu cycles, we don't want to be chopping and changing arrays, which could get quite large. Instead, # we iterate over an array which we are sure won't change out from underneath us. unless @lazy_current_queue @lazy_current_queue = @lazy_queue @lazy_queue = [] end completed = 0 begin get_lazy_fiber.resume completed += 1 end while (@lazy_queue.count > 0 or @lazy_current_queue.count > 0) and completed < count end def onopen(&blk) if @opened begin blk.call ensure @opened = false end end end def onmessage(&blk) if @messaged.size > 0 begin @messaged.each do |x| blk.call(x.to_s) end ensure @messaged = [] end end end def onclose(&blk) if @closed begin blk.call ensure end end end end line = 0 end end