From 53807b03c15596b9ce9b1183028b359614100bc8 Mon Sep 17 00:00:00 2001 From: pvincent Date: Mon, 16 Sep 2024 10:28:29 +0400 Subject: [PATCH] monkey_patcher + live_constant --- app/views/layouts/application.html.erb | 2 +- app/views/scores/index.html.erb | 3 + config/application.rb | 20 +-- config/environments/development.rb | 17 +-- config/initializers/hot_changes.rb | 9 -- config/initializers/monkey_patcher.rb | 14 --- config/initializers/monkey_patches.rb | 12 ++ config/initializers/rails_live_reload.rb | 13 +- config/initializers/semantic_logger.rb | 21 ++++ lib/hot/constants.rb | 104 --------------- lib/hot/live.rb | 8 -- lib/live/constants.rb | 13 ++ lib/live/definable.rb | 118 ++++++++++++++++++ lib/monkey_patches/monkey_patcher.rb | 17 +++ .../rails_live_reload/watcher.rb | 29 ----- lib/semantic/basic_formatter.rb | 4 +- lib/semantic/dev_loader.rb | 76 ----------- ...ansi_dimensions.rb => fancy_dimensions.rb} | 2 +- .../{ansi_formatter.rb => fancy_formatter.rb} | 4 +- lib/semantic/notification_util.rb | 39 ------ 20 files changed, 199 insertions(+), 326 deletions(-) delete mode 100644 config/initializers/hot_changes.rb delete mode 100644 config/initializers/monkey_patcher.rb create mode 100644 config/initializers/monkey_patches.rb create mode 100644 config/initializers/semantic_logger.rb delete mode 100644 lib/hot/constants.rb delete mode 100644 lib/hot/live.rb create mode 100644 lib/live/constants.rb create mode 100644 lib/live/definable.rb create mode 100644 lib/monkey_patches/monkey_patcher.rb delete mode 100644 lib/semantic/dev_loader.rb rename lib/semantic/{ansi_dimensions.rb => fancy_dimensions.rb} (95%) rename lib/semantic/{ansi_formatter.rb => fancy_formatter.rb} (98%) delete mode 100644 lib/semantic/notification_util.rb diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 71266f1..a17ab58 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,7 +14,7 @@ <%= tag :meta, name: :viewport, content: 'width=device-width,initial-scale=1' %> <% if Rails.env.development? %> <%= tag :meta, name: 'turbo-prefetch', content: false %> - <%= tag :meta, name: 'stimulus-debug', content: Hot::Constants.stimulus_debug %> + <%= tag :meta, name: 'stimulus-debug', content: Live::Constants::STIMULUS_DEBUG %> <% end%> diff --git a/app/views/scores/index.html.erb b/app/views/scores/index.html.erb index edf3391..8fcdb77 100644 --- a/app/views/scores/index.html.erb +++ b/app/views/scores/index.html.erb @@ -2,6 +2,9 @@ List of Scores +ACTION_CONTROLLER = <%=Live::Constants::ACTION_CONTROLLER%>
+ACTION_VIEW = <%=Live::Constants::ACTION_VIEW%>
+
<%= link_to "New score", new_score_path, class: "border rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
diff --git a/config/application.rb b/config/application.rb index 5d21136..a4a7714 100644 --- a/config/application.rb +++ b/config/application.rb @@ -15,26 +15,10 @@ module EasyGoingRails # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.2 - # Please, add to the `ignore` list any other `lib` subdirectories that do - # not contain `.rb` files, or that should not be reloaded or eager loaded. - # Common ones are `templates`, `generators`, or `middleware`, for example. - # config.autoload_lib(ignore: %w[assets tasks formatters hot_constants monkey_patches]) - # config.autoload_lib(ignore: %w[assets tasks formatters monkey_patches]) - config.autoload_lib(ignore: %w[assets tasks]) + # ignoring monkey_patches because MonkeyPatcher would carry out + config.autoload_lib(ignore: %w[assets tasks monkey_patches]) # main application title defined from current module name, see module above config.application_title = module_parent.to_s.titleize - - # Configuration for the application, engines, and railties goes here. - # - # These settings can be overridden in specific environments using the files - # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - - # Customized Semantic Logger - config.rails_semantic_logger.semantic = false - config.rails_semantic_logger.add_file_appender = false end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 1646fe6..a05f34d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -77,19 +77,8 @@ Rails.application.configure do # rubocop:disable Metrics/BlockLength routes.default_url_options[:port] = ARGV[1] # ie: Procfile.dev --port PORT routes.default_url_options[:host] = '127.0.0.1' - if Rails.application.console? - config.after_initialize do - formatter = Semantic::BasicFormatter.new - SemanticLogger.add_appender(io: $stdout, formatter:) - - # FIXME: filter is useful! - # SemanticLogger.add_appender(io: $stdout, - # formatter:, - # filter: ->(log) { !formatter.reject(log) }) - end - elsif Rails.application.server? - config.after_initialize do - config.dev_loader = Semantic::DevLoader.new('toto1') - end + Rails.configuration.after_initialize do + # SemanticLogger.add_appender(io: $stdout, formatter: Semantic::BasicFormatter.new) + SemanticLogger.add_appender(io: $stdout, formatter: Semantic::FancyFormatter.new) end end diff --git a/config/initializers/hot_changes.rb b/config/initializers/hot_changes.rb deleted file mode 100644 index 2e997e1..0000000 --- a/config/initializers/hot_changes.rb +++ /dev/null @@ -1,9 +0,0 @@ -return unless Rails.application.server? - -Rails.application.config.after_initialize do - Hot::Constants.on_change(:log_active_record) { |bool| ActiveRecord::Base.logger.level = bool ? :debug : :fatal } - Hot::Constants.on_change(:log_action_view) do |bool| - ActionView::Base.logger.level = bool ? :debug : :fatal - Rails.application.config.dev_loader.launch - end -end diff --git a/config/initializers/monkey_patcher.rb b/config/initializers/monkey_patcher.rb deleted file mode 100644 index 467ead6..0000000 --- a/config/initializers/monkey_patcher.rb +++ /dev/null @@ -1,14 +0,0 @@ -return unless Rails.application.server? - -# puts 'MonkeyPatcher runs:' -patches = Dir.glob(Rails.root.join('lib', 'monkey_patches', '**', '*.rb')) -patches.each do |file| - # puts "🐵 patching... #{Pathname.new(file).relative_path_from Rails.root}" - require file -end - -# puts case patches.count -# when 0 then 'No patch found' -# when 1 then '1 successful patch applied' -# else "#{patches.count} successful patches applied" -# end diff --git a/config/initializers/monkey_patches.rb b/config/initializers/monkey_patches.rb new file mode 100644 index 0000000..638d02e --- /dev/null +++ b/config/initializers/monkey_patches.rb @@ -0,0 +1,12 @@ +return unless Rails.application.server? || Rails.application.console? + +require_relative '../../lib/monkey_patches/monkey_patcher' + +MonkeyPatcher.run do |patch| + case patch + when 'action_dispatch/middleware/debug_exceptions.rb' then Rails.application.server? + when 'rails_live_reload/watcher.rb' then Rails.application.server? && Rails.env.development? + when /^semantic/ then true + else false + end +end diff --git a/config/initializers/rails_live_reload.rb b/config/initializers/rails_live_reload.rb index cfbf84f..0cac7a9 100644 --- a/config/initializers/rails_live_reload.rb +++ b/config/initializers/rails_live_reload.rb @@ -1,16 +1,11 @@ return unless defined?(RailsLiveReload) && Rails.env.development? +# RailsLiveReload RailsLiveReload.configure do |config| - # HOT Constants - config.watch(%r{/\.env$}, reload: :always) - config.watch(%r{/\.env\.sample$}) - # config.watch(%r{/config/initializers/hot_changes.rb$}) - - # USEFUL for tailwind changes!!!! - config.watch %r{app/assets/builds/tailwind.css}, reload: :always - - # Rk: prevent any reload from files ending with '.tailwind.css' + # Tailwind CSS + # First rule prevents reloading from asset files ending with '.tailwind.css' config.watch %r{(app|vendor)/(assets|javascript)/.+\.(css|js|html|png|jpg)(?") - singleton_class.undef_method(dkey) - @hot_definitions.delete(key) - end - - definitions_to_add = new_definitions.except(*@hot_definitions.keys) - definitions_to_add.each { |key, value| define_method(:add, key, value) } - end - - def load_values - logger.debug('--load_values') - - new_env = Dotenv.parse - constants_to_delete = HOTENV.except(*new_env.keys) - constants_to_delete.each do |name, _| - # FIXME: default should read default and type from @old_definitions - type, default = @old_definitions[name] - logger.info("constant <#{name}> reverts to default value <#{default}> of type <#{type}>") - end - - constants_to_add = new_env.except(*HOTENV.keys) - constants_to_add.each do |constant| - logger.info("constant to add <#{constant}>") - end - - HOTENV.replace new_env - LISTENERS.each_pair { |k, b| perform_change(k, b) } - end - - def on_change(key, &block) - LISTENERS[key.downcase.to_sym] = block - perform_change(key, block) - end - - private - - def define_method(mode, key, value) - dkey = key.downcase - method = infer_method_from_value(value) - - inferred_type = method.name.to_s.split('_')[1] - logger.info("#{mode} method <#{dkey}> of type <#{inferred_type}> with default value <#{value}> ") - - singleton_class.define_method(dkey) { method.call(key, value) } - @hot_definitions.store(key, dkey) - end - - def perform_change(key, block) - old_value = nil # TODO: remember last previous value - new_value = method(key).call - block.call(new_value, old_value) - end - - def load_boolean(key, default) = HOTENV.fetch(key, default).to_s.downcase == 'true' - def load_integer(key, default) = HOTENV.fetch(key, default).to_i - def load_string(key, default) = HOTENV.fetch(key, default) - - def infer_method_from_value(value) - case value.downcase - when /^(true|false)$/ then method(:load_boolean) - when /^\d+$/ then method(:load_integer) - else method(:load_string) - end - end - end - - initialize # done once on first require, cause this is just a module (not a class!) - end -end diff --git a/lib/hot/live.rb b/lib/hot/live.rb deleted file mode 100644 index ebce05f..0000000 --- a/lib/hot/live.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Hot - # Hot Live constants - class Live - def initialize - puts 'Hot Live constants initialized' - end - end -end diff --git a/lib/live/constants.rb b/lib/live/constants.rb new file mode 100644 index 0000000..eded9fa --- /dev/null +++ b/lib/live/constants.rb @@ -0,0 +1,13 @@ +module Live + # My live constants + module Constants + extend Definable + + STIMULUS_DEBUG = boolean false + ACTION_VIEW = boolean true + ACTION_CONTROLLER = boolean true + + MY_INTEGER = integer 8 + MY_STRING = string 'titi' + end +end diff --git a/lib/live/definable.rb b/lib/live/definable.rb new file mode 100644 index 0000000..50909da --- /dev/null +++ b/lib/live/definable.rb @@ -0,0 +1,118 @@ +require 'dotenv' + +$definable_thread_group ||= ThreadGroup.new + +module Live + # offers typed constant defintions with default value, by using lots of introspecting... + module Definable + MAIN_CSS = 'app/assets/stylesheets/application.css'.freeze # useful to trigger :reload_all from RailsLiveReload + + def integer(default = 0) = define_type_from_callee(caller[0], :integer, default) + def boolean(default = true) = define_type_from_callee(caller[0], :boolean, default) # rubocop:disable Style/OptionalBooleanParameter + def string(default = '') = define_type_from_callee(caller[0], :string, default) + + def reload_from_env + logger.debug('reload from env') + + @env_values = Dotenv.parse(*Dotenv::Rails.files) + changes = 0 + prefix = 'Constant' + env_values.each_pair do |constant, raw| + next unless definitions.include?(constant) + + value = typed_value(definitions[constant][:type], raw, definitions[constant][:default]) + next unless value != definitions[constant][:value] + + define_value(constant, value) + logger.warn "#{prefix} overriden from environment:#{Semantic::AnsiColors::CLEAR} #{constant} = #{value.ai}" + changes += 1 + end + definitions.except(*env_values.keys).each_pair do |constant, options| + default = options[:default] + next unless options[:value] != default + + define_value(constant, default) + logger.warn "#{prefix} restored:#{Semantic::AnsiColors::CLEAR} #{constant} = #{default.ai}" + changes += 1 + end + + return unless changes.positive? + + # TODO: ... + # changes=[] + # changes << {kind: :overriden, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false} + # changes << {kind: :restored, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false} + # ActiveSupport::Notifications.instrument 'rolling.live_constant', this: changes + + return unless RailsLiveReload.watcher + + FileUtils.touch(MAIN_CSS) # triggering RailsLiveReload + end + + private + + def logger = @logger ||= SemanticLogger[self] + def env_values = @env_values ||= Dotenv.parse + def definitions = @definitions ||= {} + + def define_value(constant, value) + definitions[constant][:value] = value + remove_const(constant) + const_set(constant, value) + end + + # origin (or caller[0]) helps fetching the constant name from source code introspection + def define_type_from_callee(origin, type, default) + @@class_origin ||= self # rubocop:disable Style/ClassVars + @listener ||= start_listener + + file, line = origin.split(':') + constant = introspect_constant_from_file(file, line.to_i - 1) + raw_value = env_values.fetch(constant, nil) + value = typed_value(type, raw_value, default) + definitions[constant] = { type:, default:, value: } + + # logger.debug('new definitions', definitions) + value + end + + def start_listener + $definable_thread_group.list.each(&:kill) + $definable_thread_group.add(Thread.new do + listener = Listen.to(Rails.root, only: /^\.env\.?/) do + @@class_origin.reload_from_env + rescue StandardError + nil + end + listener.start + end) + end + + def typed_value(type, raw, default) + return default if raw.nil? + + case type + when :integer then raw.to_i + when :boolean then raw.upcase == 'TRUE' + else raw + end + end + + # returns current directory of this source code + def dir_source_location + return @dir_source_location if defined?(@dir_source_location) + + *paths, _ = Live.const_source_location(:Definable).first.split('/') + @dir_source_location = paths.join('/') + end + + def introspect_constant_from_file(file, line) + *dir_file, _ = file.split('/') + dir_file = dir_file.join('/') + raise "unexpected directory: #{dir_file} != #{dir_source_location}" unless dir_file == dir_source_location + + @lines ||= File.readlines(file) # cached source code + @lines[line].match(/\s*(.\w+)/)[1] # TODO: should be uppercase! + end + end +end diff --git a/lib/monkey_patches/monkey_patcher.rb b/lib/monkey_patches/monkey_patcher.rb new file mode 100644 index 0000000..9c304be --- /dev/null +++ b/lib/monkey_patches/monkey_patcher.rb @@ -0,0 +1,17 @@ +class MonkeyPatcher + class << self + MONKEY_PATCHES = Rails.root.join('lib', 'monkey_patches') + def run(&) + patches = Dir.glob(MONKEY_PATCHES.join('**', '*.rb')) + patches.each do |patch| + file = Pathname.new(patch).relative_path_from(MONKEY_PATCHES).to_s + next if file == 'monkey_patcher.rb' # filter off its own file! + + if !block_given? || yield(file) + puts "🐵 patching... #{file}" + require file + end + end + end + end +end diff --git a/lib/monkey_patches/rails_live_reload/watcher.rb b/lib/monkey_patches/rails_live_reload/watcher.rb index 06dfd35..7e1a17f 100644 --- a/lib/monkey_patches/rails_live_reload/watcher.rb +++ b/lib/monkey_patches/rails_live_reload/watcher.rb @@ -1,9 +1,4 @@ module RailsLiveReload - ENV_FILE = Rails.root.join('.env').to_s - ENV_SAMPLE_FILE = Rails.root.join('.env.sample').to_s - INITIALIZER = Rails.root.join('config/initializers/hot_changes.rb').to_s - CHECKSUMS = {} # rubocop:disable Style/MutableConstant - # MonkeyPath Watcher class Watcher def initialize @@ -19,29 +14,5 @@ module RailsLiveReload start_socket start_listener end - - def reload_all - before_reload(files) - data = { event: RailsLiveReload::INTERNAL[:socket_events][:reload], files: }.to_json - @sockets.each { |socket, _| socket.puts data } # rubocop:disable Style/HashEachMethods - end - - private - - def before_reload(files) - perform_when_change(files, ENV_SAMPLE_FILE) { Hot::Constants.load_definitions } - perform_when_change(files, ENV_FILE) { Hot::Constants.load_values } - perform_when_change(files, INITIALIZER) { load Rails.root.join('config', 'initializers', 'hot_changes.rb') } - end - - def perform_when_change(files, key, &) - return unless files.include?(key) - - current_checksum = files[key] - return unless current_checksum != CHECKSUMS[key] - - yield - CHECKSUMS[key] = current_checksum - end end end diff --git a/lib/semantic/basic_formatter.rb b/lib/semantic/basic_formatter.rb index bd34053..c15ab67 100644 --- a/lib/semantic/basic_formatter.rb +++ b/lib/semantic/basic_formatter.rb @@ -4,7 +4,6 @@ module Semantic ANSI_REVERSED_WARNING = "\e[0;30;43m".freeze ANSI_REVERSED_ERROR = "\e[1;30;41m".freeze ANSI_GRAY = "\e[90m".freeze - # Return the complete log level name in uppercase def initialize super(time_format: '%H:%M:%S', @@ -75,7 +74,8 @@ module Semantic self.logger = logger if @time_format - [time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ') + [time, level, process_info, tags, named_tags, duration, name, message, payload, + exception].compact.join(' ') else [tags, named_tags, duration, name, message, payload, exception].compact.join(' ') end diff --git a/lib/semantic/dev_loader.rb b/lib/semantic/dev_loader.rb deleted file mode 100644 index 8a38b3b..0000000 --- a/lib/semantic/dev_loader.rb +++ /dev/null @@ -1,76 +0,0 @@ -module Semantic - # use the Zeitwerk autoloader to reattach_appender for development autoreloading feature - class DevLoader - def initialize(session_key) - @session_key = session_key - @subscribers = {} - - RailsSemanticLogger::ActionController::LogSubscriber.logger.level = :fatal # useful for remanent Rack::Log started - - launch - end - - def launch - once_and_reload do - append_ansi_formatter - - Semantic::NotificationUtil.clear_subscribers(/\.action_controller$/) - Semantic::NotificationUtil.clear_subscribers(/\.action_view$/) - reset_subscribers - - register_action_controller - register_action_view - end - end - - private - - def once_and_reload(&) - yield - Rails.autoloaders.main.on_load('ApplicationController', &) - end - - def append_ansi_formatter - SemanticLogger.clear_appenders! - formatter = Semantic::AnsiFormatter.new - SemanticLogger.add_appender(io: $stdout, - formatter:, - filter: ->(log) { !formatter.reject(log) }) - end - - def register_action_controller - sub_instance = Semantic::Subscribers::ActionController.new(@session_key) - register_hook(sub_instance, :start_processing) - register_hook(sub_instance, :process_action, :finish_processing) - register_hook(sub_instance, :redirect_to) - %i[send_file send_data halted_callback unpermitted_parameters send_stream write_fragment - read_fragment expire_fragment exist_fragment?].each do |hook| - register_hook(sub_instance, hook, :any_hook) - end - end - - def register_action_view - sub_instance = Semantic::Subscribers::ActionView.new - %i[render_template render_partial render_collection render_layout].each do |hook| - register_hook(sub_instance, hook) - end - end - - def register_hook(sub_instance, hook, method = hook) - @subscribers[sub_instance.class] ||= [] - @subscribers[sub_instance.class] << ActiveSupport::Notifications.subscribe("#{hook}.#{sub_instance.event_group}") do |event| - sub_instance.send(method, event) - end - end - - def reset_subscribers - return if @subscribers.empty? - - @subscribers.each_pair do |clazz, subs| - # puts "reset #{subs.size} subscribers for class <#{clazz}>" - subs.each { |sub| ActiveSupport::Notifications.unsubscribe(sub) } - subs.clear - end - end - end -end diff --git a/lib/semantic/ansi_dimensions.rb b/lib/semantic/fancy_dimensions.rb similarity index 95% rename from lib/semantic/ansi_dimensions.rb rename to lib/semantic/fancy_dimensions.rb index cb0620f..307e0e1 100644 --- a/lib/semantic/ansi_dimensions.rb +++ b/lib/semantic/fancy_dimensions.rb @@ -2,7 +2,7 @@ require 'ostruct' module Semantic # extra dimensions for customizing the logging format - module AnsiDimensions + module FancyDimensions def self.new(rails: '╣x╠', before: 0, after: 0, terminus: false) OpenStruct.new(rails:, before:, after:, terminus:) # rubocop:disable Style/OpenStructUse end diff --git a/lib/semantic/ansi_formatter.rb b/lib/semantic/fancy_formatter.rb similarity index 98% rename from lib/semantic/ansi_formatter.rb rename to lib/semantic/fancy_formatter.rb index 74ab1eb..54ceeb5 100644 --- a/lib/semantic/ansi_formatter.rb +++ b/lib/semantic/fancy_formatter.rb @@ -4,7 +4,7 @@ require 'json' module Semantic # wraps meanwhile takes care of ansi colors - class AnsiFormatter < SemanticLogger::Formatters::Color + class FancyFormatter < SemanticLogger::Formatters::Color include AnsiColors TAG_NONE = ''.freeze @@ -179,7 +179,7 @@ module Semantic clazz = colorize("#{exc.class}\n", color_map[:fatal]) message = colorize(exc.message.chomp(''), color_map[:error]) - backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) # TODO: backtrace_cleaner might be optionally disable from HotConstant + backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) # TODO: backtrace_cleaner might be optionally disable from Live::Constant "#{clazz}#{message}#{backtrace}" end diff --git a/lib/semantic/notification_util.rb b/lib/semantic/notification_util.rb deleted file mode 100644 index 2c7c4b5..0000000 --- a/lib/semantic/notification_util.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Semantic - module NotificationUtil - class << self - # pattern could be either a string 'start_processing.action_controller' or a regex /\.action_controller$/ - # FIXME: weird behaviour, order impact!!!! - # For instance: - # OK - # NotificationUtil.clear_subscribers(/\.action_controller$/) - # NotificationUtil.clear_subscribers(/\.action_view$/) - # NOPE - # NotificationUtil.clear_subscribers(/\.action_view$/) - # NotificationUtil.clear_subscribers(/\.action_controller$/) - def clear_subscribers(pattern) - ActiveSupport::LogSubscriber.subscribers.each { |sub| unattach(sub, pattern) } - end - - private - - def subscriber_patterns(subscriber) - subscriber.patterns.respond_to?(:keys) ? subscriber.patterns.keys : subscriber.patterns - end - - def unattach(subscriber, pattern) - subscriber_patterns(subscriber).each do |sub_pattern| - ActiveSupport::Notifications.notifier.listeners_for(sub_pattern).each do |sub| - next unless sub.instance_variable_get(:@delegate) == subscriber - next unless pattern.match(sub_pattern) - - puts "FOUND subscriber=#{subscriber} for sub_pattern=#{sub_pattern} with logger #{subscriber.logger.name}" - puts subscriber.class.module_parent.const_source_location(subscriber.class.to_s)&.first - - ActiveSupport::Notifications.unsubscribe(sub) - end - end - # ActiveSupport::LogSubscriber.subscribers.delete(subscriber) - end - end - end -end