From f77a0ceeb847b606b1ec07f7de2a47bd89dc5618 Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Thu, 18 Apr 2024 14:43:14 +0200 Subject: [PATCH 1/6] Engine server hook --- lib/tailwindcss/engine.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 4b9b9fdc..4fead1a6 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -13,5 +13,14 @@ class Engine < ::Rails::Engine config.app_generators do |g| g.template_engine :tailwindcss end + + server do + tailwind_pid = fork do + exec(*Tailwindcss::Commands.watch_command(always: true)) + end + at_exit do + Process.kill(:INT, tailwind_pid) + end + end end end From 534a84036fc3acfd19dc8b6b952ebdafe427b868 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 27 Apr 2024 12:39:00 -0400 Subject: [PATCH 2/6] The tailwindcss watch process must not steal keystrokes which would break the debugger. --- lib/tailwindcss/engine.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 4fead1a6..ead1c9b9 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -16,7 +16,11 @@ class Engine < ::Rails::Engine server do tailwind_pid = fork do - exec(*Tailwindcss::Commands.watch_command(always: true)) + # To avoid stealing keystrokes from the debug gem's IRB console in the main process (which + # needs to be able to read from $stdin), we use `IO.open(..., 'r+')`. + IO.popen(Tailwindcss::Commands.watch_command, 'r+') do |io| + IO.copy_stream(io, $stdout) + end end at_exit do Process.kill(:INT, tailwind_pid) From 355ed6248b022f34f48f3dff0880732718b6334d Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Mon, 29 Apr 2024 15:26:17 +0200 Subject: [PATCH 3/6] Use ServerProcess class --- lib/tailwindcss-rails.rb | 1 + lib/tailwindcss/engine.rb | 11 +--- lib/tailwindcss/server_process.rb | 99 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 lib/tailwindcss/server_process.rb diff --git a/lib/tailwindcss-rails.rb b/lib/tailwindcss-rails.rb index 112809c5..867663c6 100644 --- a/lib/tailwindcss-rails.rb +++ b/lib/tailwindcss-rails.rb @@ -5,3 +5,4 @@ module Tailwindcss require_relative "tailwindcss/version" require_relative "tailwindcss/engine" require_relative "tailwindcss/commands" +require_relative "tailwindcss/server_process" diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index ead1c9b9..96fb1a7f 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -15,16 +15,7 @@ class Engine < ::Rails::Engine end server do - tailwind_pid = fork do - # To avoid stealing keystrokes from the debug gem's IRB console in the main process (which - # needs to be able to read from $stdin), we use `IO.open(..., 'r+')`. - IO.popen(Tailwindcss::Commands.watch_command, 'r+') do |io| - IO.copy_stream(io, $stdout) - end - end - at_exit do - Process.kill(:INT, tailwind_pid) - end + ServerProcess.start end end end diff --git a/lib/tailwindcss/server_process.rb b/lib/tailwindcss/server_process.rb new file mode 100644 index 00000000..adbb2503 --- /dev/null +++ b/lib/tailwindcss/server_process.rb @@ -0,0 +1,99 @@ +module Tailwindcss + class ServerProcess + attr_reader :server, :pid + + def self.start + new.start + end + + def start + @server = Server.new(self) + @pid = fork do + monitor_server + exit_hook + # Using IO.popen(command, 'r+') will avoid watch_command read from $stdin. + # If we use system(*command) instead, IRB and Debug can't read from $stdin + # correctly bacause some keystrokes will be taken by watch_command. + IO.popen(Commands.watch_command, 'r+') do |io| + IO.copy_stream(io, $stdout) + end + end + Process.detach pid + + server.monitor_process + server.exit_hook + end + + def stop + return if dead? + + Process.kill(:INT, pid) + Process.wait(pid) + rescue Errno::ECHILD, Errno::ESRCH + end + + def dead? + Process.wait(pid, Process::WNOHANG) + false + rescue Errno::ECHILD, Errno::ESRCH + true + end + + private + + def monitor_server + Thread.new do + loop do + if server.dead? + puts "Tailwind detected server has gone away" + exit + end + sleep 2 + end + end + end + + def exit_hook + at_exit do + puts "Stopping tailwind..." + server.stop + end + end + + class Server + attr_reader :process, :pid + + def initialize(process) + @process = process + @pid = Process.pid + end + + def monitor_process + Thread.new do + loop do + if process.dead? + puts "Detected tailwind has gone away, stopping server..." + exit + end + sleep 2 + end + end + end + + def exit_hook + at_exit do + process.stop + end + end + + def dead? + Process.ppid != pid + end + + def stop + Process.kill(:INT, pid) + rescue Errno::ECHILD, Errno::ESRCH + end + end + end +end From eed53430d80e0faf4b3ba5c7add683a3904f5e77 Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Mon, 29 Apr 2024 23:08:38 +0200 Subject: [PATCH 4/6] Add config.tailwindcss.server_process option --- lib/tailwindcss/engine.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 96fb1a7f..99e0c82e 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -2,6 +2,9 @@ module Tailwindcss class Engine < ::Rails::Engine + config.tailwindcss = ActiveSupport::OrderedOptions.new + config.tailwindcss.server_process = Rails.env.development? + initializer "tailwindcss.assets" do Rails.application.config.assets.precompile += %w( inter-font.css ) end @@ -15,7 +18,7 @@ class Engine < ::Rails::Engine end server do - ServerProcess.start + ServerProcess.start if config.tailwindcss.server_process end end end From 7265220a4daef3a9cf4b650260c5b264006083de Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Mon, 29 Apr 2024 23:33:30 +0200 Subject: [PATCH 5/6] Enable server process only for new installs --- lib/install/tailwindcss.rb | 10 ++++++++++ lib/tailwindcss/engine.rb | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/install/tailwindcss.rb b/lib/install/tailwindcss.rb index 413f86b8..40f18746 100644 --- a/lib/install/tailwindcss.rb +++ b/lib/install/tailwindcss.rb @@ -1,5 +1,6 @@ APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb") CENTERING_CONTAINER_INSERTION_POINT = /^\s*<%= yield %>/.freeze +DEVELOPMENT_ENVIRONMENT_CONFIG_PATH = Rails.root.join("config/environments/development.rb") if APPLICATION_LAYOUT_PATH.exist? say "Add Tailwindcss include tags and container element in application layout" @@ -16,6 +17,15 @@ say %( Add <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> within the tag in your custom layout.) end +if DEVELOPMENT_ENVIRONMENT_CONFIG_PATH.exist? + say "Enable Tailwindcss server process in development" + insert_into_file DEVELOPMENT_ENVIRONMENT_CONFIG_PATH.to_s, <<~ERB.indent(2), before: /^end$/ + + # Automatically watch and build Tailwindcss when 'rails server' is started. + config.tailwindcss.server_process = true + ERB +end + say "Build into app/assets/builds" empty_directory "app/assets/builds" keep_file "app/assets/builds" diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 99e0c82e..fb60f1e3 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -3,7 +3,7 @@ module Tailwindcss class Engine < ::Rails::Engine config.tailwindcss = ActiveSupport::OrderedOptions.new - config.tailwindcss.server_process = Rails.env.development? + config.tailwindcss.server_process = false # Rails.env.development? initializer "tailwindcss.assets" do Rails.application.config.assets.precompile += %w( inter-font.css ) From b4879aa498b8e48fc25bda5b325276ecbe7f660e Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Mon, 29 Apr 2024 23:59:22 +0200 Subject: [PATCH 6/6] Ensure ServerProcess is started only once --- lib/tailwindcss/server_process.rb | 74 ++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/tailwindcss/server_process.rb b/lib/tailwindcss/server_process.rb index adbb2503..fbca382c 100644 --- a/lib/tailwindcss/server_process.rb +++ b/lib/tailwindcss/server_process.rb @@ -6,20 +6,12 @@ def self.start new.start end - def start + def initialize @server = Server.new(self) - @pid = fork do - monitor_server - exit_hook - # Using IO.popen(command, 'r+') will avoid watch_command read from $stdin. - # If we use system(*command) instead, IRB and Debug can't read from $stdin - # correctly bacause some keystrokes will be taken by watch_command. - IO.popen(Commands.watch_command, 'r+') do |io| - IO.copy_stream(io, $stdout) - end - end - Process.detach pid + end + def start + @pid = existing_process || start_process server.monitor_process server.exit_hook end @@ -41,6 +33,37 @@ def dead? private + def existing_process + if (pid = Pidfile.pid) + begin + Process.kill 0, pid + pid + rescue Errno::ESRCH + # Process does not exist + rescue Errno::EPERM + # Ignore process owned by another user + end + end + end + + def start_process + pid = fork do + Pidfile.write + monitor_server + exit_hook + # Using IO.popen(command, 'r+') will avoid watch_command read from $stdin. + # If we use system(*command) instead, IRB and Debug can't read from $stdin + # correctly bacause some keystrokes will be taken by watch_command. + IO.popen(Commands.watch_command, 'r+') do |io| + IO.copy_stream(io, $stdout) + end + ensure + Pidfile.delete + end + Process.detach pid + pid + end + def monitor_server Thread.new do loop do @@ -60,6 +83,33 @@ def exit_hook end end + module Pidfile + def self.path + Rails.root.join("tmp", "pids", "tailwindcss.txt") + end + + def self.read + File.read(path, mode: "rb:UTF-8") + rescue Errno::ENOENT + # File does not exist + end + + def self.write + File.write(path, Process.pid, mode: "wb:UTF-8") + end + + def self.delete + File.exist?(path) && File.delete(path) + end + + def self.pid + Integer(read) + rescue ArgumentError, TypeError + # Invalid content + delete + end + end + class Server attr_reader :process, :pid