diff --git a/dev/sentry.cr b/dev/sentry.cr new file mode 100644 index 00000000..86d3261c --- /dev/null +++ b/dev/sentry.cr @@ -0,0 +1,109 @@ +module Sentry + FILE_TIMESTAMPS = {} of String => String # {file => timestamp} + + class ProcessRunner + getter app_process : (Nil | Process) = nil + property process_name : String + property should_build = true + property files = [] of 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, + should_build = true) + @files = files + @should_build = should_build + @should_kill = false + @app_built = false + end + + private def build_app_process + puts "🤖 compiling #{process_name}..." + build_args = @build_args + if build_args.size > 0 + Process.run(@build_command, build_args, shell: true, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + else + Process.run(@build_command, shell: true, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + end + end + + private def create_app_process + app_process = @app_process + if app_process.is_a? Process + unless app_process.terminated? + puts "🤖 killing #{process_name}..." + app_process.kill + end + end + + puts "🤖 starting #{process_name}..." + run_args = @run_args + if run_args.size > 0 + @app_process = Process.new(@run_command, run_args, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + else + @app_process = Process.new(@run_command, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + end + end + + private def get_timestamp(file : String) + File.stat(file).mtime.to_s("%Y%m%d%H%M%S") + end + + # Compiles and starts the application + # + def start_app + return create_app_process unless @should_build + build_result = build_app_process() + if build_result && build_result.success? + @app_built = true + create_app_process() + elsif !@app_built # if build fails on first time compiling, then exit + puts "🤖 Compile time errors detected. SentryBot shutting down..." + exit 1 + end + end + + # Scans all of the `@files` + # + def scan_files + file_changed = false + app_process = @app_process + files = @files + Dir.glob(files) do |file| + timestamp = get_timestamp(file) + if FILE_TIMESTAMPS[file]? && FILE_TIMESTAMPS[file] != timestamp + FILE_TIMESTAMPS[file] = timestamp + file_changed = true + puts "🤖 #{file}" + elsif FILE_TIMESTAMPS[file]?.nil? + puts "🤖 watching file: #{file}" + FILE_TIMESTAMPS[file] = timestamp + file_changed = true if (app_process && !app_process.terminated?) + end + end + + start_app() if (file_changed || app_process.nil?) + end + + def run + puts "🤖 Your SentryBot is vigilant. beep-boop..." + + loop do + if @should_kill + puts "🤖 Powering down your SentryBot..." + break + end + scan_files + sleep 1 + end + end + + def kill + @should_kill = true + end + end +end diff --git a/dev/sentry_cli.cr b/dev/sentry_cli.cr new file mode 100644 index 00000000..6e3f4530 --- /dev/null +++ b/dev/sentry_cli.cr @@ -0,0 +1,99 @@ +require "option_parser" +require "yaml" +require "./sentry" + +process_name = nil + +begin + shard_yml = YAML.parse File.read("shard.yml") + name = shard_yml["name"]? + process_name = name.as_s if name +rescue e +end + +build_args = [] of String +build_command = "crystal build ./src/#{process_name}.cr" +run_args = [] of String +run_command = "./#{process_name}" +files = ["./src/**/*.cr", "./src/**/*.ecr"] +files_cleared = false +show_help = false +should_build = true + +OptionParser.parse! do |parser| + parser.banner = "Usage: ./sentry [options]" + parser.on( + "-n NAME", + "--name=NAME", + "Sets the name of the app process (current name: #{process_name})") { |name| process_name = name } + parser.on( + "-b COMMAND", + "--build=COMMAND", + "Overrides the default build command") { |command| build_command = command } + parser.on( + "--build-args=ARGS", + "Specifies arguments for the build command") do |args| + args_arr = args.strip.split(" ") + build_args = args_arr if args_arr.size > 0 + end + parser.on( + "--no-build", + "Skips the build step") { should_build = false } + parser.on( + "-r COMMAND", + "--run=COMMAND", + "Overrides the default run command") { |command| run_command = command } + parser.on( + "--run-args=ARGS", + "Specifies arguments for the run command") do |args| + args_arr = args.strip.split(" ") + run_args = args_arr if args_arr.size > 0 + end + parser.on( + "-w FILE", + "--watch=FILE", + "Overrides default files and appends to list of watched files") do |file| + unless files_cleared + files.clear + files_cleared = true + end + files << file + end + parser.on( + "-i", + "--info", + "Shows the values for build/run commands, build/run args, and watched files") do + puts " + name: #{process_name} + build: #{build_command} + build args: #{build_args} + run: #{run_command} + run args: #{run_args} + files: #{files} + " + end + parser.on( + "-h", + "--help", + "Show this help") do + puts parser + exit 0 + end +end + +if process_name + process_runner = Sentry::ProcessRunner.new( + process_name: process_name.as(String), + build_command: build_command, + run_command: run_command, + build_args: build_args, + run_args: run_args, + should_build: should_build, + files: files + ) + + process_runner.run +else + puts "🤖 Sentry error: 'name' not given and not found in shard.yml" + exit 1 +end diff --git a/sentry b/sentry new file mode 100755 index 00000000..aeb90127 Binary files /dev/null and b/sentry differ