Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New amber watch config #865

Closed
wants to merge 30 commits into from
Closed

New amber watch config #865

wants to merge 30 commits into from

Conversation

faustinoaq
Copy link
Contributor

@faustinoaq faustinoaq commented Jun 16, 2018

Description of the Change

This PR depends on #864 and #857

This PR adds new amber watch config using .amber.yml file

vokoscreen-2018-04-14_21-23-04

Alternate Designs

Keep current process_runner.cr and force npm/webpack commands 😅

Benefits

Possible Drawbacks

All tasks require files and commands fields, for one-time tasks (without files) you can use an empty array files: []

No cli flags, so, things like amber watch -w "./config/**/*.cr" are not supported /cc @damianham

See: https://github.com/damianham/amber_render_module#usage

@faustinoaq
Copy link
Contributor Author

This PR is based on new-exception-page branch, see: #864

@faustinoaq faustinoaq mentioned this pull request Jun 16, 2018
@faustinoaq faustinoaq requested a review from a team June 16, 2018 18:41
@faustinoaq faustinoaq requested a review from a team June 16, 2018 18:42
- "crystal build -o bin/<%= @name %> src/<%= @name %>.cr -p --no-color"
- "bin/<%= @name %>"

client: # required: these files changes trigger browser reloading
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be required? I generally do almost no front end code, what if I don't want watch to do anything?

Copy link
Contributor Author

@faustinoaq faustinoaq Jun 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robacarp You're right, actually this field can be optional, this implementation works without a client field

@faustinoaq
Copy link
Contributor Author

faustinoaq commented Jun 20, 2018

This new config works pretty nice and fix many issues, although, Does someone have any idea how to add specs for this? 😅

Copy link
Member

@robacarp robacarp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of special cases in here for the server and client watcher, but it seems like that shouldn't be necessary. The one switch that I can see which needs to be made is simply: run a command on the server, or send a message down a channel. What about changing the way the configuration is written to be more generic?

watch:
    meaningless_name:
      files:
        - src/**/*.cr
        - # etc
      notify_client: true   # sends a message through the channel
      commands:
         - shards build app # every command treated the same, and run in parallel

Would it clean up the watcher code and config?

loop do
scan_files
sleep 1
if watch_object = CLI.config.watch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is watch_config a better name than watch_object?

if watch_object = CLI.config.watch
run_watcher(watch_object)
else
error "Can't find watch settings, please check your .amber.yml file"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this error message be more helpful? Better yet, can this be a warning and then some sane default watch run anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenshot_20180621_133658

Yeah, nice idea, I'm already working on that 👍

log "Compile time errors detected. Shutting down..."
exit 1
private def run_watcher(watch_object)
watch_object.each do |key, value|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it makes sense to iterate here. Why not just do something like:

server_config = watch_object["server"]
spawn watcher("server", server_config["files"], server_config["commands"]

debug "Watching #{file_counter} #{key} files"
kill_processes(key)
commands.each do |command|
if key == "server" && command == commands.first?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic isn't obvious to me, why is the first command different from the rest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that other tasks are blocked until your amber app compiles,

Should we change that behaivior? 😅

I think your idea of running task on parallel would be nice 👍

@processes.each do |process|
process.kill unless process.terminated?
private def check_directories
Dir.mkdir_p("bin")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this create bin/?

Copy link
Contributor Author

@faustinoaq faustinoaq Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because bin/ dir is needed to store the generated binary

Useful when your tasks are like crystal build src/myapp.cr -o bin/myapp, so you don't need to execute mkdir -p first because amber watch already does this, just like shards build

process = Helpers.run(command, error: error_io)
PROCESSES << {process, "server"}
loop do
if process.terminated?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this loop contains far too much logic all in one place. It may help to refactor to use early-exits instead, but I don't know. It's hard to even see what's going on here, but something like this might be possible:

loop do
   sleep 1
   next if process.terminated?
   next if error_io.empty?
   # etc
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new method handle_terminaded_process:

Now the loop looks like this:

        loop do
          if process.terminated?
            handle_terminaded_process(process, error_io)
            break
          end
          sleep 1
        end

- "crystal build -o bin/<%= @name %> src/<%= @name %>.cr -p --no-color"
- "bin/<%= @name %>"

client: # optional: these files changes trigger browser reloading
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Optional: File changes triggers the browser to reload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! 👍

- "public/**/*"
commands: []

webpack: # optional: compiles assets using webpack
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the new Amber watch was going to decouple webpack meaning you can choose which ever front end builder you want? Or is this a simple task?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a simple task, Webpack removal is addressed here: #866

File.info(file).modification_time.to_s("%Y%m%d%H%M%S")
private def handle_error(error_output)
kill_processes("server")
puts error_output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User CLI::Logger

@@ -2,3 +2,27 @@ type: app
database: <%= @database %>
language: <%= @language %>
model: <%= @model %>
watch:
server: # required: the first command for this task is blocking
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean required: the first command for this task is blockin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means that other tasks are blocked until your amber app compiles

Maybe we should change the message, WDYT? 😅

@faustinoaq
Copy link
Contributor Author

faustinoaq commented Jun 21, 2018

There are a lot of special cases in here for the server and client watcher, but it seems like that shouldn't be necessary. The one switch that I can see which needs to be made is simply: run a command on the server, or send a message down a channel. What about changing the way the configuration is written to be more generic?

@robacarp Nice idea, let me try 👍

@faustinoaq
Copy link
Contributor Author

@robacarp I have a comment about your idea:

watch:
    meaningless_name:
      files:
        - src/**/*.cr
        - # etc
      notify_client: true   # This is already done by "client" field
      commands:
         - shards build app # Should we run this on parallel? what about next command
         - bin/app               # This requires app build first, WDYT?

@faustinoaq
Copy link
Contributor Author

faustinoaq commented Jun 21, 2018

      notify_client: true   # Nice, idea, this replaces "client" field

I got it!, I think I can implement this by appending all fields (meaningless_name) files with notify_client: true to the client watcher 👍

@faustinoaq
Copy link
Contributor Author

faustinoaq commented Jun 21, 2018

      commands:
         - shards build app # Should we run this on parallel? what about next command
         - bin/app               # This requires app build first, WDYT?

If we use parallel tasks, then I guess we should require something like this:

      commands:
         - shards build app && bin/app

WDYT?

@eliasjpr
Copy link
Contributor

@faustinoaq I would say not to mess with the feature too much, and iterate i will open issues with additional thoughts so we can merge this

@faustinoaq
Copy link
Contributor Author

@eliasjpr Yeah, no problem, now v0.8.0 is already released by @robacarp , so I'll fix this feature for v0.8.1 👍

I just realize @robacarp idea is simpler and nicer 😅

Copy link
Contributor

@eliasjpr eliasjpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments. I feel that the ProcessRunner can bit split into smaller structs/classes its doing too much and know too much about Watcher.

@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers is a module and the .wait call makes it look like is an instance of an object and the method .run is deceiving since its just creating an instance of Process. I like that this method has some helpful arguments defaults and in that case I recommend to use class CLIProcess < Process and overwrite the run method with defaults and call super on it.

require "process"

class CLIProcess
 CLI_IO = Process::Redirect::Inherit 

 def self.instance(command, shell = true, input = CLI_IO, output = CLI_IO, error = CLI_IO)
    Process.new(command, shell: shell, input: input, output: output, error: error)
  end
end
CLIProcess.instance("crystal db/seeds.cr", shell: true).wait

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks cleaner, Nice idea! 👍

"bin/#{app_name}"
]
}
}

def initialize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this initializer since is not doing anything

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove the initialize then I can't use Config.new because it complains about no matching parameters 😅

Let me add a comment about this 👍

error "Error in watch configuration. #{ex.message}"
exit 1
end
watch_config.each do |key, value|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be its own method and be able to loop to all config keys including watch_config["server"]

Copy link
Contributor Author

@faustinoaq faustinoaq Jun 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied @robacarp suggestion here, although, the new refactoring won't require this 😅

private def run_watcher(watch_config)
begin
server_config = watch_config["server"]
spawn watcher("server", server_config["files"], server_config["commands"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is server is being spawn separately and not just loop over each key as you doing below? Does the order matter? if yes, this should be moved to its own method run_server_watcher

The watch_config should be an instance variable!

Copy link
Contributor Author

@faustinoaq faustinoaq Jun 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied @robacarp suggestion here, although, the new refactoring won't require this 😅

elsif !@app_running
log "Compile time errors detected. Shutting down..."
exit 1
if watch_config = CLI.config.watch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be moved to initializer as a guard clause. watch

def initialize
  raise AMBER_YAML_ERROR unless CLI.config.amber_yml_exists?
  @amber_yaml =  CLI.config.amber_yaml
end

Actually the loading and check of this should be in Amber::CLI module and not here.

And have an instance of ProcessRunner in Amber::CLI

module Amber::CLI
  def self.process_runner
    ProcessRunner.new if amber_yml_loaded?
  end

  def amber_yaml_loaded?
     raise AMBER_YAML_ERROR unless CLI.config.amber_yml_exists?
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestion, let me apply this refactoring as well 👍

private def build_app_process
log "Building project #{project_name}..."
Amber::CLI::Helpers.run(@build_command)
private def handle_terminaded_process(process, error_io)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a TerminatedProcessHandler classs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this useless now, I gonna use parallel processes (like @robacarp suggested), no channel nor concurrency management would be required 👍

end
end

private def error_server(error_output)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body of this method should be moved to a ErrorServer class/module

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, let me split this file 👍


private def handle_error(error_output)
kill_processes("server")
puts error_output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI::Logger?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I forgot this one 😅

@notify_counter = 0
@notify_counter_channel = Channel(Int32).new
@notify_channel = Channel(Nil).new
@host = Helpers.settings.host
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come we are getting settings information from a Helper module and not Amber::CLI environment settings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is using include Enviroment, I know this is a bad practice, because even I was confused about this when I tried to figure out where Helpers.settings is defined, So, I gonna use Amber::CLI 👍

Amber::CLI::Helpers.run("npm install --loglevel=error && npm run watch", wait: false)
node_log "Watching public directory"
private def run_command(command, key)
if key == "server"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is "server" special?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did such a complex stuff here, I'm already fixing this 👍

@drujensen drujensen mentioned this pull request Jul 5, 2018
@robacarp robacarp closed this Aug 24, 2018
@drujensen
Copy link
Member

@robacarp Why was this closed? I'm confused as to the state of this issue.

@elorest
Copy link
Member

elorest commented Oct 6, 2018

Why was this closed? Looks a little complex, but there's some good stuff in here.

@faustinoaq Will there be another PR with some of this? Also will autoreload be added back soon?

@robacarp
Copy link
Member

robacarp commented Oct 6, 2018

@elorest @drujensen This was closed for cleanup -- @faustinoaq did some great work, but doesn't have time these days to contribute as much. If someone wants to pick up where he left of, 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants