From 8abe79c923db1e0d8f062dbf0c2ec2d29b6a0113 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 10:19:34 -0500 Subject: [PATCH 01/23] Added new exception page --- .../templates/error/src/pipes/error.cr.ecr | 4 + src/amber/controller/error.cr | 42 +- src/amber/exceptions/exception_page.cr | 113 +++ src/amber/exceptions/exception_page.ecr | 860 ++++++++++++++++++ .../exception_page_client_script.js | 53 ++ .../exception_page_server_script.js | 21 + src/amber/exceptions/exceptions.cr | 9 - 7 files changed, 1067 insertions(+), 35 deletions(-) create mode 100644 src/amber/exceptions/exception_page.cr create mode 100644 src/amber/exceptions/exception_page.ecr create mode 100644 src/amber/exceptions/exception_page_client_script.js create mode 100644 src/amber/exceptions/exception_page_server_script.js diff --git a/src/amber/cli/templates/error/src/pipes/error.cr.ecr b/src/amber/cli/templates/error/src/pipes/error.cr.ecr index 2f730fdcd..1819c570e 100644 --- a/src/amber/cli/templates/error/src/pipes/error.cr.ecr +++ b/src/amber/cli/templates/error/src/pipes/error.cr.ecr @@ -7,6 +7,10 @@ module Amber def call(context : HTTP::Server::Context) raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? call_next(context) + rescue ex : Amber::Exceptions::ValidationFailed | Amber::Exceptions::InvalidParam + context.response.status_code = 400 + error = Amber::Controller::Error.new(context, ex) + context.response.print(error.bad_request) rescue ex : Amber::Exceptions::Forbidden context.response.status_code = 403 error = <%= class_name %>Controller.new(context, ex) diff --git a/src/amber/controller/error.cr b/src/amber/controller/error.cr index d75889afe..bce90fa7a 100644 --- a/src/amber/controller/error.cr +++ b/src/amber/controller/error.cr @@ -1,4 +1,5 @@ require "./base" +require "../exceptions/exception_page" module Amber::Controller class Error < Base @@ -7,49 +8,38 @@ module Amber::Controller @context.response.content_type = content_type end - def not_found - response_format(@ex.message) + def bad_request + response_format end - def internal_server_error - response_format("ERROR: #{internal_server_error_message}") + def forbidden + response_format end - def forbidden - response_format(@ex.message) + def not_found + response_format + end + + def internal_server_error + response_format end private def content_type if context.request.headers["Accept"]? request.headers["Accept"].split(",").first else - "text/html" + "text/plain" end end - private def internal_server_error_message - # IMPORTANT: #inspect_with_backtrace will fail in some situations which breaks the tests. - # Even if you call @ex.callstack you'll notice that backtrace is nil. - # #backtrace? is supposed to be safe but it exceptions anyway. - # Please don't remove this without verifying that crystal core has been fixed first. - @ex.inspect_with_backtrace - rescue ex : IndexError - @ex.message - rescue ex - <<-ERROR - Original Error: #{@ex.message} - Error during 'inspect_with_backtrace': #{ex.message} - ERROR - end - - private def response_format(message) + private def response_format case content_type when "application/json" - {"error": message}.to_json + {"error": @ex.message}.to_json when "text/html" - "
#{message}
" + Amber::Exceptions::ExceptionPageClient.new(@context, @ex).to_s else - message + @ex.message end end end diff --git a/src/amber/exceptions/exception_page.cr b/src/amber/exceptions/exception_page.cr new file mode 100644 index 000000000..39e1c342f --- /dev/null +++ b/src/amber/exceptions/exception_page.cr @@ -0,0 +1,113 @@ +require "ecr" +require "../../environment/settings" + +module Amber::Exceptions + include Environment + + class ExceptionPage + struct Frame + property app : String, + args : String, + context : String, + index : Int32, + file : String, + line : Int32, + info : String, + snippet = [] of Tuple(Int32, String, Bool) + + def initialize(@app, @context, @index, @file, @args, @line, @info, @snippet) + end + end + + @params : Hash(String, String) + @headers : Hash(String, Array(String)) + @session : Hash(String, HTTP::Cookie) + @method : String + @path : String + @message : String + @query : String + @reload_code = "" + @frames = [] of Frame + + def initialize(context : HTTP::Server::Context, @message : String) + @params = context.request.query_params.to_h + @headers = context.response.headers.to_h + @method = context.request.method + @path = context.request.path + @url = "#{context.request.host_with_port}#{context.request.path}" + @query = context.request.query_params.to_s + @session = context.response.cookies.to_h + end + + def generate_frames_from(message : String) + generated_frames = [] of Frame + if frames = message.scan(/\s([^\s\:]+):(\d+)([^\n]+)/) + frames.each_with_index do |frame, index| + snippets = [] of Tuple(Int32, String, Bool) + file = frame[1] + filename = file.split('/').last + linenumber = frame[2].to_i + linemsg = "#{file}:#{linenumber}#{frame[3]}" + if File.exists?(file) + lines = File.read_lines(file) + lines.each_with_index do |code, codeindex| + if (codeindex + 1) <= (linenumber + 5) && (codeindex + 1) >= (linenumber - 5) + highlight = (codeindex + 1 == linenumber) ? true : false + snippets << {codeindex + 1, code, highlight} + end + end + end + context = "all" + app = case file + when .includes?("/crystal/") + "crystal" + when .includes?("/amber/") + "amber" + when .includes?("lib/") + "shards" + else + context = "app" + Amber.settings.name.as(String) + end + generated_frames << Frame.new(app, context, index, file, linemsg, linenumber, filename, snippets) + end + end + if self.class.name == "ExceptionPageServer" + generated_frames.reverse + else + generated_frames + end + end + + ECR.def_to_s "#{__DIR__}/exception_page.ecr" + end + + class ExceptionPageClient < ExceptionPage + EX_ECR_SCRIPT = "lib/amber/src/amber/exceptions/exception_page_client_script.js" + + def initialize(context : HTTP::Server::Context, @ex : Exception) + super(context, @ex.message) + @title = "Error #{context.response.status_code}" + @frames = generate_frames_from(@ex.inspect_with_backtrace) + @reload_code = File.read(File.join(Dir.current, EX_ECR_SCRIPT)) + end + end + + class ExceptionPageServer < ExceptionPage + def initialize(context : HTTP::Server::Context, message : String, @error_id : String) + super(context, message) + @title = "Build Error" + @method = "Server" + @path = Dir.current + @frames = generate_frames_from(message) + @reload_code = ExceptionPageServerScript.new(@error_id).to_s + end + end + + class ExceptionPageServerScript + def initialize(@error_id : String) + end + + ECR.def_to_s "#{__DIR__}/exception_page_server_script.js" + end +end diff --git a/src/amber/exceptions/exception_page.ecr b/src/amber/exceptions/exception_page.ecr new file mode 100644 index 000000000..869c14514 --- /dev/null +++ b/src/amber/exceptions/exception_page.ecr @@ -0,0 +1,860 @@ + +<%- +accent = "#e57310" +highlight = "#ebe1d7" +red_highlight = "#ffe5e5" +light_accent = "#c0bda0" +gray = "#807e60" +line_color = "#eee" +text_color = "#271708" +logo_uri = "data:image/png;base64,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" +monospace_font = "menlo, consolas, monospace" +-%> + + + + <% + details = @message.split('\n') + headline = details.first + %> + + <%= @title %> at <%= @method %> <%= @path %> - <%= headline %> + + + + + +
+ +
+
+ <%= @title %> + at <%= @method %> <%= @path %> +
+

<%= HTML.escape(headline).gsub("'", '\'').gsub(""", '"') %>

+
+ + See raw message + +
<%- details.each do |detail| -%><%= HTML.escape(detail).gsub("'", '\'').gsub(""", '"') %>
+<%- end -%>
+
+
+
+ <% if !@frames.empty? %> +
+
+ <% @frames.each do |frame| %> +
+ + + <%- if (snippet = frame.snippet) && !snippet.empty? -%> +
<%- snippet.each do |index, line, highlight| -%>
+<%= index %><%= HTML.escape(line.rstrip).gsub("'", '\'').gsub(""", '"') %>
+<%- end -%>
+
+ <%- else -%> +
No code available.
+ <%- end -%> + + <% if !frame.args.blank? %> +
+ + <% if app = frame.app %> + <%= app %> + <% end %> + <%= frame.info %> + +
<%= HTML.escape(frame.args).gsub("'", '\'').gsub(""", '"') %>
+
+ <% else %> +
+
+ <% if app = frame.app %> + <%= app %> + <% end %> + <%= frame.info %> +
+
+ <% end %> +
+ <% end %> +
+ +
+
+ + +
+ +
    + <% @frames.each do |frame| %> +
  • + +
  • + <% end %> +
+
+
+ <% end %> +
+ +
+ <% if @params && !@params.empty? %> +
+ Params + <% @params.each do |key, value| %> +
+
<%= key %>
+
<%= value.inspect %>
+
+ <% end %> +
+ <% end %> + +
+ Request info + +
+
URI:
+
<%= @url %>
+
+ +
+
Query string:
+
<%= @query %>
+
+
+ +
+ Headers + <% @headers.each do |key, value| %> +
+
<%= key %>
+
<%= value %>
+
+ <% end %> +
+ + <% if (session = @session) && !session.empty? %> +
+ Session + <% session.each do |key, value| %> +
+
<%= key %>
+
<%= value.inspect %>
+
+ <% end %> +
+ <% end %> +
+ + + + diff --git a/src/amber/exceptions/exception_page_client_script.js b/src/amber/exceptions/exception_page_client_script.js new file mode 100644 index 000000000..b5b49b40b --- /dev/null +++ b/src/amber/exceptions/exception_page_client_script.js @@ -0,0 +1,53 @@ +if ('WebSocket' in window) { + (function () { + /** + * Allows to reload the browser when the server connection is lost + */ + function tryReload() { + var request = new XMLHttpRequest(); + request.open('GET', window.location.href, true); + request.onreadystatechange = function () { + if (request.readyState == 4) { + if (request.status == 0) { + setTimeout(function () { + tryReload(); + }, 1000) + } else { + window.location.reload(); + } + } + }; + request.send(); + } + /** + * Listen server file reload + */ + function refreshCSS() { + var sheets = [].slice.call(document.getElementsByTagName('link')); + var head = document.getElementsByTagName('head')[0]; + for (var i = 0; i < sheets.length; ++i) { + var elem = sheets[i]; + var rel = elem.rel; + if (elem.href && typeof rel != 'string' || rel.length == 0 || rel.toLowerCase() == 'stylesheet') { + head.removeChild(elem); + var url = elem.href.replace(/(&|\\?)_cacheOverride=\\d+/, ''); + elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf()); + head.appendChild(elem); + } + } + } + var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://'; + var address = protocol + window.location.host + '/client-reload'; + var socket = new WebSocket(address); + socket.onmessage = function (msg) { + if (msg.data == 'reload') { + window.location.reload(); + } else if (msg.data == 'refreshcss') { + refreshCSS(); + } + }; + socket.onclose = function () { + tryReload(); + } + })(); +} diff --git a/src/amber/exceptions/exception_page_server_script.js b/src/amber/exceptions/exception_page_server_script.js new file mode 100644 index 000000000..b6059f6ac --- /dev/null +++ b/src/amber/exceptions/exception_page_server_script.js @@ -0,0 +1,21 @@ +var reload = document.querySelector('[role~="reload-toggle"]'); +var reloadTimeout; +recursiveReload(); +on(reload, 'click', reloadOnclick); +function recursiveReload() { + var request = new XMLHttpRequest(); + request.open('GET', window.location.href, true); + request.onreadystatechange = function () { + if (request.readyState === 4) { + var errorId = request.getResponseHeader('Client-Reload'); + if (errorId === '<%= @error_id %>') { + reloadTimeout = setTimeout(function () { + recursiveReload(); + }, 1000) + } else if (errorId === 'true') { + window.location.reload(); + } + } + }; + request.send(); +} diff --git a/src/amber/exceptions/exceptions.cr b/src/amber/exceptions/exceptions.cr index e0d246cef..80d18900a 100644 --- a/src/amber/exceptions/exceptions.cr +++ b/src/amber/exceptions/exceptions.cr @@ -3,13 +3,6 @@ require "colorize" module Amber module Exceptions class Base < Exception - getter status_code : Int32 = 500 - - def set_response(response) - response.headers["Content-Type"] = "text/plain" - response.print message - response.status_code = status_code - end end class Environment < Exception @@ -43,14 +36,12 @@ module Amber class RouteNotFound < Base def initialize(request) - @status_code = 404 super("The request was not found. #{request.method} - #{request.path}") end end class Forbidden < Base def initialize(message : String?) - @status_code = 403 super(message || "Action is Forbidden.") end end From 994e730a2ca6e7e4a5bef19c2906ee1b25b4908b Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 11:54:20 -0500 Subject: [PATCH 02/23] Simplify Helpers.run --- src/amber/cli/commands/database.cr | 2 +- src/amber/cli/helpers/helpers.cr | 8 ++------ src/amber/cli/recipes/recipe.cr | 2 +- src/amber/cli/templates/template.cr | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/amber/cli/commands/database.cr b/src/amber/cli/commands/database.cr index dec41cf93..9314cbbcd 100644 --- a/src/amber/cli/commands/database.cr +++ b/src/amber/cli/commands/database.cr @@ -55,7 +55,7 @@ module Amber::CLI when "create" Micrate.logger.info create_database when "seed" - Helpers.run("crystal db/seeds.cr", wait: true, shell: true) + Helpers.run("crystal db/seeds.cr", shell: true).wait Micrate.logger.info "Seeded database" when "migrate" begin diff --git a/src/amber/cli/helpers/helpers.cr b/src/amber/cli/helpers/helpers.cr index f7e9641be..3016254af 100644 --- a/src/amber/cli/helpers/helpers.cr +++ b/src/amber/cli/helpers/helpers.cr @@ -33,11 +33,7 @@ module Amber::CLI::Helpers File.write("./config/application.cr", application.gsub("require \"amber\"", replacement)) end - def self.run(command, wait = true, shell = true) - if wait - Process.run(command, shell: shell, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) - else - Process.new(command, shell: shell, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) - end + def self.run(command, shell = true, input = Process::Redirect::Inherit, output = Process::Redirect::Inherit, error = Process::Redirect::Inherit) + Process.new(command, shell: shell, input: input, output: output, error: error) end end diff --git a/src/amber/cli/recipes/recipe.cr b/src/amber/cli/recipes/recipe.cr index cb9afb45d..54db651d4 100644 --- a/src/amber/cli/recipes/recipe.cr +++ b/src/amber/cli/recipes/recipe.cr @@ -58,7 +58,7 @@ module Amber::Recipes App.new(name, options.d, options.t, options.m, options.r).render(directory, list: true, color: true) if options.deps? info "Installing Dependencies" - Amber::CLI::Helpers.run("cd #{name} && shards update") + Amber::CLI::Helpers.run("cd #{name} && shards update").wait end end when "controller" diff --git a/src/amber/cli/templates/template.cr b/src/amber/cli/templates/template.cr index 416b14de1..84e0f284d 100644 --- a/src/amber/cli/templates/template.cr +++ b/src/amber/cli/templates/template.cr @@ -51,7 +51,7 @@ module Amber::CLI App.new(name, options.d, options.t, options.m).render(directory, list: true, color: true) if options.deps? info "Installing Dependencies" - Helpers.run("cd #{name} && shards update") + Helpers.run("cd #{name} && shards update").wait end end when "migration" From 792538267c652e6c5a2d3c09fdac66bb1c507e8c Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 11:55:18 -0500 Subject: [PATCH 03/23] Add new amber watch config --- src/amber/cli/commands/watch.cr | 17 +- src/amber/cli/config.cr | 23 ++- src/amber/cli/helpers/file_watcher.cr | 20 ++ src/amber/cli/helpers/process_runner.cr | 251 +++++++++++++++++------- src/amber/cli/helpers/sentry.cr | 93 --------- 5 files changed, 221 insertions(+), 183 deletions(-) create mode 100644 src/amber/cli/helpers/file_watcher.cr delete mode 100644 src/amber/cli/helpers/sentry.cr diff --git a/src/amber/cli/commands/watch.cr b/src/amber/cli/commands/watch.cr index d1eb0df8d..ba33b7085 100644 --- a/src/amber/cli/commands/watch.cr +++ b/src/amber/cli/commands/watch.cr @@ -1,28 +1,27 @@ require "cli" -require "../helpers/sentry" +require "../helpers/process_runner" module Amber::CLI class MainCommand < ::Cli::Supercommand command "w", aliased: "watch" - class Watch < Sentry::SentryCommand - command_name "watch" - + class Watch < ::Cli::Command class Options bool "--no-color", desc: "# Disable colored output", default: false help end class Help - header "Starts amber development server and rebuilds on file changes" - caption "# Starts amber development server and rebuilds on file changes" + header <<-HEADER + Starts amber development server and rebuilds on file changes. + See `.amber.yml` for more settings. + HEADER end def run CLI.toggle_colors(options.no_color?) - options.watch << "./config/**/*.cr" - options.watch << "./src/views/**/*.slang" - super + process_runner = Helpers::ProcessRunner.new + process_runner.run end end end diff --git a/src/amber/cli/config.cr b/src/amber/cli/config.cr index 217d2bb65..3ca31585f 100644 --- a/src/amber/cli/config.cr +++ b/src/amber/cli/config.cr @@ -1,3 +1,5 @@ +require "yaml" + module Amber::CLI def self.config if File.exists? AMBER_YML @@ -5,14 +7,20 @@ module Amber::CLI else Config.new end + rescue ex : YAML::ParseException + logger.error "Couldn't parse #{AMBER_YML} file", "Watcher", :red + exit 1 end class Config - property database : String = "pg" - property language : String = "slang" - property model : String = "granite" - property recipe : (String | Nil) = nil - property recipe_source : (String | Nil) = nil + alias Watch = Hash(String, Hash(String, Array(String))) + + getter database : String = "pg" + getter language : String = "slang" + getter model : String = "granite" + getter recipe : String? + getter recipe_source : String? + getter watch : Watch? def initialize end @@ -21,8 +29,9 @@ module Amber::CLI database: {type: String, default: "pg"}, language: {type: String, default: "slang"}, model: {type: String, default: "granite"}, - recipe: String | Nil, - recipe_source: String | Nil + recipe: String?, + recipe_source: String?, + watch: Watch? ) end end diff --git a/src/amber/cli/helpers/file_watcher.cr b/src/amber/cli/helpers/file_watcher.cr new file mode 100644 index 000000000..e86710080 --- /dev/null +++ b/src/amber/cli/helpers/file_watcher.cr @@ -0,0 +1,20 @@ +module Amber + class FileWatcher + @file_timestamps = {} of String => String + + private def get_timestamp(file : String) + File.info(file).mtime.to_s("%Y%m%d%H%M%S") + end + + def scan_files(files) + Dir.glob(files) do |file| + timestamp = get_timestamp(file) + if @file_timestamps[file]? != timestamp + @file_timestamps[file] = timestamp + yield file + end + end + end + end + end + \ No newline at end of file diff --git a/src/amber/cli/helpers/process_runner.cr b/src/amber/cli/helpers/process_runner.cr index 14e2a7aa6..febf81e08 100644 --- a/src/amber/cli/helpers/process_runner.cr +++ b/src/amber/cli/helpers/process_runner.cr @@ -1,114 +1,217 @@ +require "http" require "./helpers" +require "../config" +require "./file_watcher" +require "../../exceptions/exception_page" -module Sentry - class ProcessRunner - property processes = [] of Process - property process_name : String - property files = [] of String - @logger : Amber::Environment::Logger - FILE_TIMESTAMPS = {} of String => String - - def initialize( - @process_name : String, - @build_command : String, - @run_command : String, - @build_args : Array(String) = [] of String, - @run_args : Array(String) = [] of String, - files = [] of String, - @logger = Amber::CLI.logger - ) - @files = files - @npm_process = false - @app_running = false +module Amber::CLI::Helpers + include Environment + + struct ProcessRunner + PROCESSES = [] of {Process, String} + + @host : String + @port : Int32 + @file_watcher : FileWatcher + + def initialize + @watch_running = false + @wait_build = Channel(Bool).new + @server_files_changed = false + @notify_counter = 0 + @notify_counter_channel = Channel(Int32).new + @notify_channel = Channel(Nil).new + @host = Helpers.settings.host + @port = Helpers.settings.port + @file_watcher = FileWatcher.new + + at_exit do + kill_processes + end + + Signal::INT.trap do + Signal::INT.reset + exit + end end def run - loop do - scan_files - sleep 1 + if watch_object = CLI.config.watch + run_watcher(watch_object) + else + error "Can't find watch settings, please check your .amber.yml file" + exit 1 end + rescue ex : KeyError + error "Error in watch configuration. #{ex.message}" + exit 1 end - # Compiles and starts the application - def start_app - build_result = build_app_process - if build_result.is_a? Process::Status - if build_result.success? - stop_all_processes - create_all_processes - @app_running = true - elsif !@app_running - log "Compile time errors detected. Shutting down..." - exit 1 + private def run_watcher(watch_object) + watch_object.each do |key, value| + next if key == "client" + files = value["files"] + commands = value["commands"] + if key != "server" + @notify_counter += 1 end + spawn watcher(key, files, commands) end + @notify_counter_channel.send @notify_counter + @notify_counter = @notify_counter_channel.receive + sleep end - private def scan_files + private def watcher(key, files, commands) + if key != "server" + @notify_channel.receive + end + if files.empty? + commands.each do |command| + run_command(command, key) + end + else + loop do + scan_files(key, files, commands) + @watch_running = true if key == "server" + sleep 1 + end + end + end + + private def scan_files(key, files, commands) file_counter = 0 - Dir.glob(files) do |file| - timestamp = get_timestamp(file) - if FILE_TIMESTAMPS[file]? != timestamp - if @app_running - log "File changed: #{file.colorize(:light_gray)}" - end - FILE_TIMESTAMPS[file] = timestamp - file_counter += 1 + @file_watcher.scan_files(files) do |file| + if @watch_running + debug "File changed: #{file}" end + file_counter += 1 end if file_counter > 0 - log "Watching #{file_counter} files (server reload)..." - start_app + debug "Watching #{file_counter} #{key} files" + kill_processes(key) + commands.each do |command| + if key == "server" && command == commands.first? + run_build_command(command, commands) + else + run_command(command, key) + end + end end end - private def stop_all_processes - log "Terminating app #{project_name}..." - @processes.each do |process| - process.kill unless process.terminated? + private def check_directories + Dir.mkdir_p("bin") + if !Dir.exists?("lib") + error "You need to install dependencies first, execute `shards install`" + exit 1 end - processes.clear end - private def create_all_processes - process = create_watch_process - @processes << process if process.is_a? Process - unless @npm_process - create_npm_process - @npm_process = true + private def run_build_command(command, commands) + check_directories + next_server_commands = (1...commands.size) + info "Building project #{Helpers.settings.name.colorize(:light_cyan)}" + spawn do + error_io = IO::Memory.new + process = Helpers.run(command, error: error_io) + PROCESSES << {process, "server"} + loop do + if process.terminated? + exit_status = process.wait.exit_status + if error_io.empty? + if exit_status.zero? + if @watch_running + kill_processes("server") + else + notify_next_processes + end + next_server_commands.each { @wait_build.send true } + else + next_server_commands.each { @wait_build.send false } + end + else + handle_error(error_io.to_s) + next_server_commands.each { @wait_build.send false } + end + break + end + sleep 1 + end end end - private def build_app_process - log "Building project #{project_name}..." - Amber::CLI::Helpers.run(@build_command) + private def notify_next_processes + notify_counter = @notify_counter_channel.receive + notify_counter.times { @notify_channel.send nil } + @notify_counter_channel.send 0 end - private def create_watch_process - log "Starting #{project_name}..." - Amber::CLI::Helpers.run(@run_command, wait: false, shell: false) + private def run_command(command, key) + if key == "server" + spawn do + build_sucess? = @wait_build.receive + if build_sucess? + error_io = IO::Memory.new + process = Helpers.run(command, error: error_io) + PROCESSES << {process, "server"} + loop do + if process.terminated? + unless error_io.empty? + handle_error(error_io.to_s) + end + break + end + sleep 1 + end + end + end + else + process = Helpers.run(command) + PROCESSES << {process, key} + end end - private def create_npm_process - node_log "Installing dependencies..." - Amber::CLI::Helpers.run("npm install --loglevel=error && npm run watch", wait: false) - node_log "Watching public directory" + private def kill_processes(key = nil) + PROCESSES.each do |process, owner| + if process.terminated? + PROCESSES.delete(process) + elsif owner == key || key.nil? + process.kill + end + end + end + + private def error_server(error_output) + HTTP::Server.new(@host, @port) do |context| + error_id = Digest::MD5.hexdigest(error_output) + context.response.content_type = "text/html" + context.response.status_code = 500 + context.response.headers["Client-Reload"] = [error_id] + context.response.print Amber::Exceptions::ExceptionPageServer.new(context, error_output, error_id).to_s + end end - private def get_timestamp(file : String) - File.stat(file).mtime.to_s("%Y%m%d%H%M%S") + private def handle_error(error_output) + kill_processes("server") + puts error_output + new_error_server = Process.fork do + error_server(error_output).listen(reuse_port: true) + end + PROCESSES << {new_error_server, "server"} + error "A server error has been detected see the output above, use CTRL+C to exit" end - private def project_name - process_name.capitalize.colorize(:white) + private def debug(msg) + CLI.logger.debug msg, "Watcher" end - private def log(msg) - @logger.info msg, "Watcher", :light_gray + private def info(msg) + CLI.logger.info msg, "Watcher", :light_cyan end - private def node_log(msg) - @logger.info msg, "NodeJS", :dark_gray + private def error(msg) + CLI.logger.error msg, "Watcher", :light_red end end end diff --git a/src/amber/cli/helpers/sentry.cr b/src/amber/cli/helpers/sentry.cr deleted file mode 100644 index 8c37deff4..000000000 --- a/src/amber/cli/helpers/sentry.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "cli" -require "yaml" -require "./process_runner" - -module Sentry - class SentryCommand < Cli::Command - command_name "sentry" - SHARD_YML = "shard.yml" - DEFAULT_NAME = "[process_name]" - - class Options - def self.defaults - name = Options.get_name - { - name: name, - process_name: "./bin/#{name}", - build: "mkdir -p bin && crystal build ./src/#{name}.cr -o bin/#{name}", - watch: ["./src/**/*.cr", "./src/**/*.ecr"], - } - end - - def self.get_name - if File.exists?(SHARD_YML) && - (yaml = YAML.parse(File.read SHARD_YML)) && - (name = yaml["name"]?) - name.as_s - else - DEFAULT_NAME - end - end - - string %w(-n --name), desc: "Sets the name of the app process", - default: Options.defaults[:name] - - string %w(-b --build), desc: "Overrides the default build command", - default: Options.defaults[:build] - - string "--build-args", desc: "Specifies arguments for the build command" - - string %w(-r --run), desc: "Overrides the default run command", - default: Options.defaults[:process_name] - - string "--run-args", desc: "Specifies arguments for the run command" - - array %w(-w --watch), - desc: "Overrides default files and appends to list of watched files", - default: Options.defaults[:watch] - - bool %w(-i --info), - desc: "Shows the values for build/run commands, build/run args, and watched files", - default: false - - help - end - - def run - if options.info? - puts <<-INFO - name: #{options.name?} - build: #{options.build?} - build args: #{options.build_args?} - run: #{options.run?} - run args: #{options.run_args?} - files: #{options.watch} - INFO - exit! code: 0 - end - - build_args = if ba = options.build_args? - ba.split " " - else - [] of String - end - run_args = if ra = options.run_args? - ra.split " " - else - [] of String - end - - process_runner = Sentry::ProcessRunner.new( - process_name: options.name, - build_command: options.build, - run_command: options.run, - build_args: build_args, - run_args: run_args, - files: options.watch, - logger: Amber::CLI.logger - ) - - process_runner.run - end - end -end From d148ce95f207a02eed8b2d1bb0b1c2a2c1f2953f Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 12:02:56 -0500 Subject: [PATCH 04/23] Add new watch config --- src/amber/cli/templates/app/.amber.yml.ecr | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/amber/cli/templates/app/.amber.yml.ecr b/src/amber/cli/templates/app/.amber.yml.ecr index 0214f6af2..52ab48d14 100644 --- a/src/amber/cli/templates/app/.amber.yml.ecr +++ b/src/amber/cli/templates/app/.amber.yml.ecr @@ -2,3 +2,27 @@ type: app database: <%= @database %> language: <%= @language %> model: <%= @model %> +watch: + server: # required: the first command for this task is blocking + files: + - "src/**/*.cr" + - "src/**/*.<%= @language %>" + - "config/**/*.cr" + commands: + - "crystal build -o bin/<%= @name %> src/<%= @name %>.cr -p --no-color" + - "bin/<%= @name %>" + + client: # required: these files changes trigger browser reloading + files: + - "public/**/*" + commands: [] + + webpack: # optional: compiles assets using webpack + files: [] # webpack already manage this + commands: + - "npm install --loglevel=error" + - "nom run watch" + + mytask: # Minimal valid task example + files: [] # Tasks with empty "files" execute "commands" just once. + commands: [] # Tasks with empty "commands" are ignored, except "client" task. From b65e3fbde41c04f708839a7d4beac8480ab04c93 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 12:13:23 -0500 Subject: [PATCH 05/23] Add JavaScript to support reload on html pages using default layout. --- .../templates/app/public/js/amber_reload.js | 53 +++++++++++++++++++ .../app/src/views/layouts/application.ecr.ecr | 1 + .../src/views/layouts/application.slang.ecr | 2 + 3 files changed, 56 insertions(+) create mode 100644 src/amber/cli/templates/app/public/js/amber_reload.js diff --git a/src/amber/cli/templates/app/public/js/amber_reload.js b/src/amber/cli/templates/app/public/js/amber_reload.js new file mode 100644 index 000000000..b5b49b40b --- /dev/null +++ b/src/amber/cli/templates/app/public/js/amber_reload.js @@ -0,0 +1,53 @@ +if ('WebSocket' in window) { + (function () { + /** + * Allows to reload the browser when the server connection is lost + */ + function tryReload() { + var request = new XMLHttpRequest(); + request.open('GET', window.location.href, true); + request.onreadystatechange = function () { + if (request.readyState == 4) { + if (request.status == 0) { + setTimeout(function () { + tryReload(); + }, 1000) + } else { + window.location.reload(); + } + } + }; + request.send(); + } + /** + * Listen server file reload + */ + function refreshCSS() { + var sheets = [].slice.call(document.getElementsByTagName('link')); + var head = document.getElementsByTagName('head')[0]; + for (var i = 0; i < sheets.length; ++i) { + var elem = sheets[i]; + var rel = elem.rel; + if (elem.href && typeof rel != 'string' || rel.length == 0 || rel.toLowerCase() == 'stylesheet') { + head.removeChild(elem); + var url = elem.href.replace(/(&|\\?)_cacheOverride=\\d+/, ''); + elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf()); + head.appendChild(elem); + } + } + } + var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://'; + var address = protocol + window.location.host + '/client-reload'; + var socket = new WebSocket(address); + socket.onmessage = function (msg) { + if (msg.data == 'reload') { + window.location.reload(); + } else if (msg.data == 'refreshcss') { + refreshCSS(); + } + }; + socket.onclose = function () { + tryReload(); + } + })(); +} diff --git a/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr b/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr index 17c9b245b..fd7cb6301 100644 --- a/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr +++ b/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr @@ -37,6 +37,7 @@ + <% if Amber.env.development? %><% end %> diff --git a/src/amber/cli/templates/app/src/views/layouts/application.slang.ecr b/src/amber/cli/templates/app/src/views/layouts/application.slang.ecr index f9342623a..34c919d85 100644 --- a/src/amber/cli/templates/app/src/views/layouts/application.slang.ecr +++ b/src/amber/cli/templates/app/src/views/layouts/application.slang.ecr @@ -26,4 +26,6 @@ html script src="https://code.jquery.com/jquery-3.3.1.min.js" script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" + - if Amber.env.development? + script src="/js/amber_reload.js" script src="/dist/main.bundle.js" From 7f49bd7ad3dca09e06e39c72ef4a53322639e316 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 16 Jun 2018 14:15:24 -0500 Subject: [PATCH 06/23] Add missing exception --- src/amber/pipes/error.cr | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/amber/pipes/error.cr b/src/amber/pipes/error.cr index aff74879d..8fdd493e4 100644 --- a/src/amber/pipes/error.cr +++ b/src/amber/pipes/error.cr @@ -7,6 +7,10 @@ module Amber def call(context : HTTP::Server::Context) raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? call_next(context) + rescue ex : Amber::Exceptions::ValidationFailed | Amber::Exceptions::InvalidParam + context.response.status_code = 400 + error = Amber::Controller::Error.new(context, ex) + context.response.print(error.bad_request) rescue ex : Amber::Exceptions::Forbidden context.response.status_code = 403 error = Amber::Controller::Error.new(context, ex) From 19dc8d6511d1fd5f94fe0185aa878bcad7d2e1ed Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sun, 17 Jun 2018 12:06:13 -0500 Subject: [PATCH 07/23] Fix typo --- src/amber/cli/templates/app/.amber.yml.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amber/cli/templates/app/.amber.yml.ecr b/src/amber/cli/templates/app/.amber.yml.ecr index 52ab48d14..1e36aa8b0 100644 --- a/src/amber/cli/templates/app/.amber.yml.ecr +++ b/src/amber/cli/templates/app/.amber.yml.ecr @@ -21,7 +21,7 @@ watch: files: [] # webpack already manage this commands: - "npm install --loglevel=error" - - "nom run watch" + - "npm run watch" mytask: # Minimal valid task example files: [] # Tasks with empty "files" execute "commands" just once. From 10e9f6093f4ac019c16a66a552d9250bfbeb0dc5 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sun, 17 Jun 2018 16:41:19 -0500 Subject: [PATCH 08/23] Replace mtime by modification_time --- src/amber/cli/helpers/file_watcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amber/cli/helpers/file_watcher.cr b/src/amber/cli/helpers/file_watcher.cr index e86710080..5b83bf664 100644 --- a/src/amber/cli/helpers/file_watcher.cr +++ b/src/amber/cli/helpers/file_watcher.cr @@ -3,7 +3,7 @@ module Amber @file_timestamps = {} of String => String private def get_timestamp(file : String) - File.info(file).mtime.to_s("%Y%m%d%H%M%S") + File.info(file).modification_time.to_s("%Y%m%d%H%M%S") end def scan_files(files) From 65f81aa13b63fc4d921f51302f602db27f478b84 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 12:18:22 -0500 Subject: [PATCH 09/23] Remove trailing whitespaces --- src/amber/cli/helpers/file_watcher.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/amber/cli/helpers/file_watcher.cr b/src/amber/cli/helpers/file_watcher.cr index 5b83bf664..28dbcdce7 100644 --- a/src/amber/cli/helpers/file_watcher.cr +++ b/src/amber/cli/helpers/file_watcher.cr @@ -1,11 +1,11 @@ module Amber class FileWatcher @file_timestamps = {} of String => String - + private def get_timestamp(file : String) File.info(file).modification_time.to_s("%Y%m%d%H%M%S") end - + def scan_files(files) Dir.glob(files) do |file| timestamp = get_timestamp(file) @@ -17,4 +17,3 @@ module Amber end end end - \ No newline at end of file From 36f7b3a75d787879005e9d9a378b83dabbb6d462 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 18:04:19 -0500 Subject: [PATCH 10/23] Client field is optional --- src/amber/cli/templates/app/.amber.yml.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amber/cli/templates/app/.amber.yml.ecr b/src/amber/cli/templates/app/.amber.yml.ecr index 1e36aa8b0..a605e0eca 100644 --- a/src/amber/cli/templates/app/.amber.yml.ecr +++ b/src/amber/cli/templates/app/.amber.yml.ecr @@ -12,7 +12,7 @@ watch: - "crystal build -o bin/<%= @name %> src/<%= @name %>.cr -p --no-color" - "bin/<%= @name %>" - client: # required: these files changes trigger browser reloading + client: # optional: these files changes trigger browser reloading files: - "public/**/*" commands: [] From 11a014f47312cf59387559ea2185f6eaf75adb10 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 18:09:38 -0500 Subject: [PATCH 11/23] Use more descriptive var name --- src/amber/cli/helpers/process_runner.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/amber/cli/helpers/process_runner.cr b/src/amber/cli/helpers/process_runner.cr index febf81e08..76073fe3a 100644 --- a/src/amber/cli/helpers/process_runner.cr +++ b/src/amber/cli/helpers/process_runner.cr @@ -110,7 +110,7 @@ module Amber::CLI::Helpers private def run_build_command(command, commands) check_directories - next_server_commands = (1...commands.size) + next_server_commands_range = (1...commands.size) info "Building project #{Helpers.settings.name.colorize(:light_cyan)}" spawn do error_io = IO::Memory.new @@ -126,13 +126,13 @@ module Amber::CLI::Helpers else notify_next_processes end - next_server_commands.each { @wait_build.send true } + next_server_commands_range.each { @wait_build.send true } else - next_server_commands.each { @wait_build.send false } + next_server_commands_range.each { @wait_build.send false } end else handle_error(error_io.to_s) - next_server_commands.each { @wait_build.send false } + next_server_commands_range.each { @wait_build.send false } end break end From dd65e57c9b5100e390cfaf0499494b0663f1954a Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 20:58:28 -0500 Subject: [PATCH 12/23] Error pipe cleanup --- src/amber/cli/templates/error.cr | 2 +- .../templates/error/src/pipes/error.cr.ecr | 50 +++++++++---------- src/amber/pipes/error.cr | 37 +++++++++----- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/amber/cli/templates/error.cr b/src/amber/cli/templates/error.cr index cf60c685b..a997136f2 100644 --- a/src/amber/cli/templates/error.cr +++ b/src/amber/cli/templates/error.cr @@ -14,7 +14,7 @@ module Amber::CLI end private def add_plugs - add_plugs :web, "plug Amber::Pipe::Error.new" + add_plugs :web, "plug #{class_name}.new" end private def add_dependencies diff --git a/src/amber/cli/templates/error/src/pipes/error.cr.ecr b/src/amber/cli/templates/error/src/pipes/error.cr.ecr index 1819c570e..cc562e6d0 100644 --- a/src/amber/cli/templates/error/src/pipes/error.cr.ecr +++ b/src/amber/cli/templates/error/src/pipes/error.cr.ecr @@ -1,29 +1,25 @@ -module Amber - module Pipe - # The Error pipe catches RouteNotFound and returns a 404. It responds based - # on the `Accepts` header as JSON or HTML. It also catches any runtime - # Exceptions and returns a backtrace in text/html format. - class Error < Base - def call(context : HTTP::Server::Context) - raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? - call_next(context) - rescue ex : Amber::Exceptions::ValidationFailed | Amber::Exceptions::InvalidParam - context.response.status_code = 400 - error = Amber::Controller::Error.new(context, ex) - context.response.print(error.bad_request) - rescue ex : Amber::Exceptions::Forbidden - context.response.status_code = 403 - error = <%= class_name %>Controller.new(context, ex) - context.response.print(error.forbidden) - rescue ex : Amber::Exceptions::RouteNotFound - context.response.status_code = 404 - error = <%= class_name %>Controller.new(context, ex) - context.response.print(error.not_found) - rescue ex : Exception - context.response.status_code = 500 - error = <%= class_name %>Controller.new(context, ex) - context.response.print(error.internal_server_error) - end - end +class <%= class_name %> < Amber::Pipe::Error + def error(ex : ValidationFailed | InvalidParam) + context.response.status_code = 400 + action = <%= class_name %>Controller.new(context, ex) + context.response.print(action.bad_request) + end + + def error(ex : Forbidden) + context.response.status_code = 403 + action = <%= class_name %>Controller.new(context, ex) + context.response.print(action.forbidden) + end + + def error(ex : RouteNotFound) + context.response.status_code = 404 + action = <%= class_name %>Controller.new(context, ex) + context.response.print(action.not_found) + end + + def error(ex) + context.response.status_code = 500 + action = <%= class_name %>Controller.new(context, ex) + context.response.print(action.internal_server_error) end end diff --git a/src/amber/pipes/error.cr b/src/amber/pipes/error.cr index 8fdd493e4..c0aba9913 100644 --- a/src/amber/pipes/error.cr +++ b/src/amber/pipes/error.cr @@ -4,25 +4,38 @@ module Amber # on the `Accepts` header as JSON or HTML. It also catches any runtime # Exceptions and returns a backtrace in text/html format. class Error < Base + include Amber::Exceptions + include Amber::Exceptions::Validator + def call(context : HTTP::Server::Context) raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? call_next(context) - rescue ex : Amber::Exceptions::ValidationFailed | Amber::Exceptions::InvalidParam + rescue ex + error(ex) + end + + def error(ex : ValidationFailed | InvalidParam) context.response.status_code = 400 - error = Amber::Controller::Error.new(context, ex) - context.response.print(error.bad_request) - rescue ex : Amber::Exceptions::Forbidden + action = Amber::Controller::Error.new(context, ex) + context.response.print(action.bad_request) + end + + def error(ex : Forbidden) context.response.status_code = 403 - error = Amber::Controller::Error.new(context, ex) - context.response.print(error.forbidden) - rescue ex : Amber::Exceptions::RouteNotFound + action = Amber::Controller::Error.new(context, ex) + context.response.print(action.forbidden) + end + + def error(ex : RouteNotFound) context.response.status_code = 404 - error = Amber::Controller::Error.new(context, ex) - context.response.print(error.not_found) - rescue ex : Exception + action = Amber::Controller::Error.new(context, ex) + context.response.print(action.not_found) + end + + def error(ex) context.response.status_code = 500 - error = Amber::Controller::Error.new(context, ex) - context.response.print(error.internal_server_error) + action = Amber::Controller::Error.new(context, ex) + context.response.print(action.internal_server_error) end end end From 08a3e17b23140f7b74c5cf726727a98ad7dbbdf7 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 21:09:53 -0500 Subject: [PATCH 13/23] Add missing context argument --- src/amber/cli/templates/error/src/pipes/error.cr.ecr | 8 ++++---- src/amber/pipes/error.cr | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/amber/cli/templates/error/src/pipes/error.cr.ecr b/src/amber/cli/templates/error/src/pipes/error.cr.ecr index cc562e6d0..a1769195d 100644 --- a/src/amber/cli/templates/error/src/pipes/error.cr.ecr +++ b/src/amber/cli/templates/error/src/pipes/error.cr.ecr @@ -1,23 +1,23 @@ class <%= class_name %> < Amber::Pipe::Error - def error(ex : ValidationFailed | InvalidParam) + def error(context, ex : ValidationFailed | InvalidParam) context.response.status_code = 400 action = <%= class_name %>Controller.new(context, ex) context.response.print(action.bad_request) end - def error(ex : Forbidden) + def error(context, ex : Forbidden) context.response.status_code = 403 action = <%= class_name %>Controller.new(context, ex) context.response.print(action.forbidden) end - def error(ex : RouteNotFound) + def error(context, ex : RouteNotFound) context.response.status_code = 404 action = <%= class_name %>Controller.new(context, ex) context.response.print(action.not_found) end - def error(ex) + def error(context, ex) context.response.status_code = 500 action = <%= class_name %>Controller.new(context, ex) context.response.print(action.internal_server_error) diff --git a/src/amber/pipes/error.cr b/src/amber/pipes/error.cr index c0aba9913..f417e9ece 100644 --- a/src/amber/pipes/error.cr +++ b/src/amber/pipes/error.cr @@ -11,28 +11,28 @@ module Amber raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? call_next(context) rescue ex - error(ex) + error(context, ex) end - def error(ex : ValidationFailed | InvalidParam) + def error(context, ex : ValidationFailed | InvalidParam) context.response.status_code = 400 action = Amber::Controller::Error.new(context, ex) context.response.print(action.bad_request) end - def error(ex : Forbidden) + def error(context, ex : Forbidden) context.response.status_code = 403 action = Amber::Controller::Error.new(context, ex) context.response.print(action.forbidden) end - def error(ex : RouteNotFound) + def error(context, ex : RouteNotFound) context.response.status_code = 404 action = Amber::Controller::Error.new(context, ex) context.response.print(action.not_found) end - def error(ex) + def error(context, ex) context.response.status_code = 500 action = Amber::Controller::Error.new(context, ex) context.response.print(action.internal_server_error) From a31f3475cd8249343b1aefb7fcba3aae2b061451 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 21:26:57 -0500 Subject: [PATCH 14/23] Fixes valid_route? method --- src/amber/pipes/error.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amber/pipes/error.cr b/src/amber/pipes/error.cr index f417e9ece..b0086b0e7 100644 --- a/src/amber/pipes/error.cr +++ b/src/amber/pipes/error.cr @@ -8,7 +8,7 @@ module Amber include Amber::Exceptions::Validator def call(context : HTTP::Server::Context) - raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route? + raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.request.valid_route? call_next(context) rescue ex error(context, ex) From ba2abe49ccc3f4457712c9a1735d572cfc39c9ed Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 21:33:59 -0500 Subject: [PATCH 15/23] Fixes error specs --- .../error/spec/controllers/{{name}}_controller_spec.cr.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amber/cli/templates/error/spec/controllers/{{name}}_controller_spec.cr.ecr b/src/amber/cli/templates/error/spec/controllers/{{name}}_controller_spec.cr.ecr index d0d386c69..d33ab4914 100644 --- a/src/amber/cli/templates/error/spec/controllers/{{name}}_controller_spec.cr.ecr +++ b/src/amber/cli/templates/error/spec/controllers/{{name}}_controller_spec.cr.ecr @@ -35,10 +35,10 @@ class <%= class_name %>ControllerTest < GarnetSpec::Controller::Test def initialize @handler = Amber::Pipe::Pipeline.new @handler.build :web do - plug Amber::Pipe::Error.new + plug <%= class_name %>.new end @handler.build :static do - plug Amber::Pipe::Error.new + plug <%= class_name %>.new end @handler.prepare_pipelines end From c4054086329c6feb5f340fea42b8cf9487536ecd Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 22:04:46 -0500 Subject: [PATCH 16/23] Remove dead sentry require --- src/amber/cli/commands/pipelines.cr | 1 - src/amber/cli/commands/routes.cr | 1 - 2 files changed, 2 deletions(-) diff --git a/src/amber/cli/commands/pipelines.cr b/src/amber/cli/commands/pipelines.cr index 7805cf884..7f73c8157 100644 --- a/src/amber/cli/commands/pipelines.cr +++ b/src/amber/cli/commands/pipelines.cr @@ -1,6 +1,5 @@ require "cli" require "shell-table" -require "../helpers/sentry" module Amber::CLI class MainCommand < ::Cli::Supercommand diff --git a/src/amber/cli/commands/routes.cr b/src/amber/cli/commands/routes.cr index 4ede6475f..4a98ec11b 100644 --- a/src/amber/cli/commands/routes.cr +++ b/src/amber/cli/commands/routes.cr @@ -1,6 +1,5 @@ require "cli" require "shell-table" -require "../helpers/sentry" module Amber::CLI class MainCommand < ::Cli::Supercommand From 235add8dcef11e3246ee43de8ddc7d8a4863a412 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 19 Jun 2018 22:14:35 -0500 Subject: [PATCH 17/23] Fix error server --- src/amber/cli/helpers/process_runner.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amber/cli/helpers/process_runner.cr b/src/amber/cli/helpers/process_runner.cr index 76073fe3a..28af26056 100644 --- a/src/amber/cli/helpers/process_runner.cr +++ b/src/amber/cli/helpers/process_runner.cr @@ -183,7 +183,7 @@ module Amber::CLI::Helpers end private def error_server(error_output) - HTTP::Server.new(@host, @port) do |context| + HTTP::Server.new do |context| error_id = Digest::MD5.hexdigest(error_output) context.response.content_type = "text/html" context.response.status_code = 500 @@ -196,7 +196,7 @@ module Amber::CLI::Helpers kill_processes("server") puts error_output new_error_server = Process.fork do - error_server(error_output).listen(reuse_port: true) + error_server(error_output).listen(@host, @port, reuse_port: true) end PROCESSES << {new_error_server, "server"} error "A server error has been detected see the output above, use CTRL+C to exit" From 25443c3d0f1697c855926dfd75c10ef0fcbc1415 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Thu, 21 Jun 2018 14:24:08 -0500 Subject: [PATCH 18/23] Add reload pipe again (still need to generate websocket reload server) --- spec/amber/pipes/reload_spec.cr | 98 ++++++++--------- src/amber/pipes/reload.cr | 4 +- src/amber/support/client_reload.cr | 165 ++++++++++++++++------------- 3 files changed, 143 insertions(+), 124 deletions(-) diff --git a/spec/amber/pipes/reload_spec.cr b/spec/amber/pipes/reload_spec.cr index c9cfd3501..46818a14a 100644 --- a/spec/amber/pipes/reload_spec.cr +++ b/spec/amber/pipes/reload_spec.cr @@ -1,49 +1,49 @@ -# TODO: remove this file after https://github.com/amberframework/amber/pull/860 is merged - -# class FakeEnvironment < Amber::Environment::Env -# def development? -# true -# end -# end - -# module Amber -# module Pipe -# describe Reload do -# headers = HTTP::Headers.new -# headers["Accept"] = "text/html" -# request = HTTP::Request.new("GET", "/reload", headers) - -# Amber::Server.router.draw :web do -# get "/reload", HelloController, :index -# end - -# context "when environment is in development mode" do -# pipeline = Pipeline.new -# pipeline.build :web do -# plug Amber::Pipe::Reload.new(FakeEnvironment.new) -# end -# pipeline.prepare_pipelines - -# it "contains injected code in response.body" do -# response = create_request_and_return_io(pipeline, request) - -# response.body.should contain "Code injected by Amber Framework" -# end -# end - -# context "when environment is NOT in development mode" do -# pipeline = Pipeline.new -# pipeline.build :web do -# plug Amber::Pipe::Reload.new -# end -# pipeline.prepare_pipelines - -# it "does not have injected reload code in response.body" do -# response = create_request_and_return_io(pipeline, request) - -# response.body.should_not contain "Code injected by Amber Framework" -# end -# end -# end -# end -# end +require "../../spec_helper" + +class FakeEnvironment < Amber::Environment::Env + def development? + true + end +end + +module Amber + module Pipe + describe Reload do + headers = HTTP::Headers.new + headers["Accept"] = "text/html" + request = HTTP::Request.new("GET", "/reload", headers) + + Amber::Server.router.draw :web do + get "/reload", HelloController, :index + end + + context "when environment is in development mode" do + pipeline = Pipeline.new + pipeline.build :web do + plug Amber::Pipe::Reload.new(FakeEnvironment.new) + end + pipeline.prepare_pipelines + + it "contains injected header in response" do + response = create_request_and_return_io(pipeline, request) + + response.headers["Client-Reload"]?.should_not be_nil + end + end + + context "when environment is NOT in development mode" do + pipeline = Pipeline.new + pipeline.build :web do + plug Amber::Pipe::Reload.new + end + pipeline.prepare_pipelines + + it "does not have injected header in response" do + response = create_request_and_return_io(pipeline, request) + + response.headers["Client-Reload"]?.should be_nil + end + end + end + end +end diff --git a/src/amber/pipes/reload.cr b/src/amber/pipes/reload.cr index 26effc5cf..5a131a330 100644 --- a/src/amber/pipes/reload.cr +++ b/src/amber/pipes/reload.cr @@ -12,12 +12,12 @@ module Amber # ``` class Reload < Base def initialize(@env : Amber::Environment::Env = Amber.env) - Support::ClientReload.new + Support::ClientReload.new.run end def call(context : HTTP::Server::Context) if @env.development? && context.format == "html" - context.response << Support::ClientReload::INJECTED_CODE + context.response.headers["Client-Reload"] = %(true) end call_next(context) end diff --git a/src/amber/support/client_reload.cr b/src/amber/support/client_reload.cr index cd258aa08..b317e67c4 100644 --- a/src/amber/support/client_reload.cr +++ b/src/amber/support/client_reload.cr @@ -1,28 +1,77 @@ +require "../cli/helpers/file_watcher" +require "../cli/helpers/helpers" +require "../cli/config" + module Amber::Support # Used by `Amber::Pipe::Reload` # # Allow clients browser reloading using WebSockets and file watchers. struct ClientReload - FILE_TIMESTAMPS = {} of String => String - WEBSOCKET_PATH = rand(0x10000000).to_s(36) - SESSIONS = [] of HTTP::WebSocket + SESSIONS = [] of HTTP::WebSocket + PROCESSES = [] of Process + AMBER_YML = ".amber.yml" + + @file_watcher = FileWatcher.new + @app_running = false def initialize - create_reload_server - @app_running = false - spawn run + at_exit do + kill_client_processes + end + end + + def config + if File.exists?(AMBER_YML) + CLI::Config.from_yaml(File.read(AMBER_YML)) + else + CLI::Config.new + end end def run + if watch_config = config.watch + run_watcher(watch_config) + else + warn "Can't find watch settings, do you want to add default watch settings? (y/n)" + if gets.to_s.lowercase == "y" + generate_config + end + exit 1 + end + rescue ex : KeyError + error "Error in watch configuration. #{ex.message}" + exit 1 + end + + private def generate_config + File.write(AMBER_YML, config.to_yaml) + end + + private def run_watcher(watch_config) + entries = watch_config["client"] + commands = entries["commands"] + files = entries["files"] + if files.empty? + run_commands(commands) + else + spawn watcher(files, commands) + end + create_reload_server + rescue ex + error "Error in watch configuration. #{ex.message}" + exit 1 + end + + private def watcher(files, commands) loop do - scan_files + scan_files(files, commands) @app_running = true sleep 1 end end private def create_reload_server - Amber::WebSockets::Server::Handler.new "/#{WEBSOCKET_PATH}" do |session| + Amber::WebSockets::Server::Handler.new "/client-reload" do |session| SESSIONS << session session.on_close do SESSIONS.delete session @@ -30,9 +79,19 @@ module Amber::Support end end - private def reload_clients(msg) - SESSIONS.each do |session| - session.@ws.send msg + def scan_files(files, commands) + file_counter = 0 + @file_watcher.scan_files(files) do |file| + if @app_running + debug "File changed: #{file}" + end + file_counter += 1 + check_file(file) + end + if file_counter > 0 + debug "Watching #{file_counter} client files..." + kill_client_processes + run_commands(commands) end end @@ -45,75 +104,35 @@ module Amber::Support end end - private def get_timestamp(file : String) - File.info(file).modification_time.to_s("%Y%m%d%H%M%S") + private def reload_clients(msg) + SESSIONS.each do |session| + session.@ws.send msg + end end - private def scan_files - file_counter = 0 - Dir.glob(["public/**/*"]) do |file| - timestamp = get_timestamp(file) - if FILE_TIMESTAMPS[file]? != timestamp - if @app_running - log "File changed: ./#{file.colorize(:light_gray)}" - end - FILE_TIMESTAMPS[file] = timestamp - file_counter += 1 - check_file(file) - end + private def run_commands(commands) + commands.each do |command| + PROCESSES << CLI::Helpers.run(command) end - if file_counter > 0 - log "Watching #{file_counter} files (browser reload)..." + end + + private def kill_client_processes + PROCESSES.each do |process| + process.kill unless process.terminated? + PROCESSES.delete(process) end end - def log(message) - Amber.logger.info(message, "Watcher", :light_gray) + private def debug(msg) + Amber.logger.debug msg, "Watcher", :light_gray + end + + private def error(msg) + Amber.logger.error msg, "Watcher", :red end - # Code from https://github.com/tapio/live-server/blob/master/injected.html - INJECTED_CODE = <<-HTML - - \n - HTML + private def warn(msg) + CLI.logger.warn msg, "Watcher", :yellow + end end end From 080f96cb70dfa1404dfe644a40d64a1a988576f2 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Thu, 21 Jun 2018 14:24:25 -0500 Subject: [PATCH 19/23] Fix layout template --- .../cli/templates/app/src/views/layouts/application.ecr.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr b/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr index fd7cb6301..b8298b264 100644 --- a/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr +++ b/src/amber/cli/templates/app/src/views/layouts/application.ecr.ecr @@ -37,7 +37,7 @@ - <% if Amber.env.development? %><% end %> + <%="<"%>%- if Amber.env.development? -%><%="<"%>%- end -%> From 0521e1d61f5606d2c7ae3dfd897218e48b886ccf Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Thu, 21 Jun 2018 14:24:46 -0500 Subject: [PATCH 20/23] Add config generator --- src/amber/cli/config.cr | 30 +++++++++++++++--- src/amber/cli/helpers/process_runner.cr | 32 ++++++++++++++------ src/amber/cli/templates/app/config/routes.cr | 1 + 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/amber/cli/config.cr b/src/amber/cli/config.cr index 3ca31585f..d3b660bde 100644 --- a/src/amber/cli/config.cr +++ b/src/amber/cli/config.cr @@ -12,6 +12,10 @@ module Amber::CLI exit 1 end + def generate_config + File.write(AMBER_YML, CLI.config.to_yaml) + end + class Config alias Watch = Hash(String, Hash(String, Array(String))) @@ -20,18 +24,34 @@ module Amber::CLI getter model : String = "granite" getter recipe : String? getter recipe_source : String? - getter watch : Watch? + getter watch : Watch = { + "server" => { + "files" => [ + "src/**/*.cr", + "src/**/*.#{@language}", + "config/**/*.cr" + ], + "commands" => [ + "shards build -p --no-color", + "bin/#{app_name}" + ] + } + } def initialize end + private def app_name + File.basename(Dir.current) + end + YAML.mapping( - database: {type: String, default: "pg"}, - language: {type: String, default: "slang"}, - model: {type: String, default: "granite"}, + database: {type: String, default: @database}, + language: {type: String, default: @language}, + model: {type: String, default: @model}, recipe: String?, recipe_source: String?, - watch: Watch? + watch: {type: Watch, default: @watch} ) end end diff --git a/src/amber/cli/helpers/process_runner.cr b/src/amber/cli/helpers/process_runner.cr index 28af26056..0a90ba428 100644 --- a/src/amber/cli/helpers/process_runner.cr +++ b/src/amber/cli/helpers/process_runner.cr @@ -36,10 +36,13 @@ module Amber::CLI::Helpers end def run - if watch_object = CLI.config.watch - run_watcher(watch_object) + if watch_config = CLI.config.watch + run_watcher(watch_config) else - error "Can't find watch settings, please check your .amber.yml file" + warn "Can't find watch settings, do you want to add default watch settings? (y/n)" + if gets.to_s.lowercase == "y" + CLI.generate_config + end exit 1 end rescue ex : KeyError @@ -47,14 +50,19 @@ module Amber::CLI::Helpers exit 1 end - private def run_watcher(watch_object) - watch_object.each do |key, value| - next if key == "client" + private def run_watcher(watch_config) + begin + server_config = watch_config["server"] + spawn watcher("server", server_config["files"], server_config["commands"]) + rescue ex + error "Error in watch configuration. #{ex.message}" + exit 1 + end + watch_config.each do |key, value| + next if key == "client" || key == "server" files = value["files"] commands = value["commands"] - if key != "server" - @notify_counter += 1 - end + @notify_counter += 1 spawn watcher(key, files, commands) end @notify_counter_channel.send @notify_counter @@ -211,7 +219,11 @@ module Amber::CLI::Helpers end private def error(msg) - CLI.logger.error msg, "Watcher", :light_red + CLI.logger.error msg, "Watcher", :red + end + + private def warn(msg) + CLI.logger.warn msg, "Watcher", :yellow end end end diff --git a/src/amber/cli/templates/app/config/routes.cr b/src/amber/cli/templates/app/config/routes.cr index f016ba192..809a30e1f 100644 --- a/src/amber/cli/templates/app/config/routes.cr +++ b/src/amber/cli/templates/app/config/routes.cr @@ -10,6 +10,7 @@ Amber::Server.configure do plug Amber::Pipe::Session.new plug Amber::Pipe::Flash.new plug Amber::Pipe::CSRF.new + # Reload clients browsers (development only) plug Amber::Pipe::Reload.new if Amber.env.development? end From a3167edac2497da4982a75a48e60358c470fd5e9 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Thu, 21 Jun 2018 14:39:19 -0500 Subject: [PATCH 21/23] Add handle_terminaded_process --- src/amber/cli/helpers/process_runner.cr | 36 ++++++++++++++----------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/amber/cli/helpers/process_runner.cr b/src/amber/cli/helpers/process_runner.cr index 0a90ba428..3d47bdf5d 100644 --- a/src/amber/cli/helpers/process_runner.cr +++ b/src/amber/cli/helpers/process_runner.cr @@ -126,22 +126,7 @@ module Amber::CLI::Helpers PROCESSES << {process, "server"} loop do if process.terminated? - exit_status = process.wait.exit_status - if error_io.empty? - if exit_status.zero? - if @watch_running - kill_processes("server") - else - notify_next_processes - end - next_server_commands_range.each { @wait_build.send true } - else - next_server_commands_range.each { @wait_build.send false } - end - else - handle_error(error_io.to_s) - next_server_commands_range.each { @wait_build.send false } - end + handle_terminaded_process(process, error_io) break end sleep 1 @@ -149,6 +134,25 @@ module Amber::CLI::Helpers end end + private def handle_terminaded_process(process, error_io) + exit_status = process.wait.exit_status + if error_io.empty? + if exit_status.zero? + if @watch_running + kill_processes("server") + else + notify_next_processes + end + next_server_commands_range.each { @wait_build.send true } + else + next_server_commands_range.each { @wait_build.send false } + end + else + handle_error(error_io.to_s) + next_server_commands_range.each { @wait_build.send false } + end + end + private def notify_next_processes notify_counter = @notify_counter_channel.receive notify_counter.times { @notify_channel.send nil } From b88fdc2966f50a38c21df07ae09a848772370180 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Thu, 21 Jun 2018 14:41:01 -0500 Subject: [PATCH 22/23] Use Titlecase --- src/amber/cli/templates/app/.amber.yml.ecr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/amber/cli/templates/app/.amber.yml.ecr b/src/amber/cli/templates/app/.amber.yml.ecr index a605e0eca..201adc84b 100644 --- a/src/amber/cli/templates/app/.amber.yml.ecr +++ b/src/amber/cli/templates/app/.amber.yml.ecr @@ -3,7 +3,7 @@ database: <%= @database %> language: <%= @language %> model: <%= @model %> watch: - server: # required: the first command for this task is blocking + server: # Required: the first command for this task is blocking files: - "src/**/*.cr" - "src/**/*.<%= @language %>" @@ -12,12 +12,12 @@ watch: - "crystal build -o bin/<%= @name %> src/<%= @name %>.cr -p --no-color" - "bin/<%= @name %>" - client: # optional: these files changes trigger browser reloading + client: # Optional: these files changes trigger browser reloading files: - "public/**/*" commands: [] - webpack: # optional: compiles assets using webpack + webpack: # Optional: compiles assets using webpack files: [] # webpack already manage this commands: - "npm install --loglevel=error" From ff5f794ae9e37f859e0ae6e6e0017dfd879056bf Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Tue, 26 Jun 2018 11:59:16 -0500 Subject: [PATCH 23/23] Use tryReload() --- src/amber/cli/templates/app/public/js/amber_reload.js | 2 +- src/amber/exceptions/exception_page_client_script.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amber/cli/templates/app/public/js/amber_reload.js b/src/amber/cli/templates/app/public/js/amber_reload.js index b5b49b40b..2e24531ec 100644 --- a/src/amber/cli/templates/app/public/js/amber_reload.js +++ b/src/amber/cli/templates/app/public/js/amber_reload.js @@ -41,7 +41,7 @@ if ('WebSocket' in window) { var socket = new WebSocket(address); socket.onmessage = function (msg) { if (msg.data == 'reload') { - window.location.reload(); + tryReload(); } else if (msg.data == 'refreshcss') { refreshCSS(); } diff --git a/src/amber/exceptions/exception_page_client_script.js b/src/amber/exceptions/exception_page_client_script.js index b5b49b40b..2e24531ec 100644 --- a/src/amber/exceptions/exception_page_client_script.js +++ b/src/amber/exceptions/exception_page_client_script.js @@ -41,7 +41,7 @@ if ('WebSocket' in window) { var socket = new WebSocket(address); socket.onmessage = function (msg) { if (msg.data == 'reload') { - window.location.reload(); + tryReload(); } else if (msg.data == 'refreshcss') { refreshCSS(); }