diff --git a/bin/NETRAVE b/bin/NETRAVE index f989160..89e5994 100644 --- a/bin/NETRAVE +++ b/bin/NETRAVE @@ -8,6 +8,8 @@ require_relative '../lib/utils/database_manager' require_relative '../lib/utils/first_run_init' require_relative '../lib/utils/utilities' require_relative '../lib/utils/logg_man' +require_relative '../lib/utils/alert_queue_manager' +require_relative '../lib/utils/alert_manager' include Utilities # rubocop:disable Style/MixinUsage # binding.b(do: 'irb') @@ -19,8 +21,11 @@ File.open('.env', 'w') {} unless File.exist?('.env') # Load environment variables from .env file Dotenv.load +# Initialize AlertQueueManager, RingBuffer, and AlertManager +@alert_queue_manager = AlertQueueManager.new(@loggman) + # Initialize DatabaseManager -db_manager = DatabaseManager.new(@loggman) +db_manager = DatabaseManager.new(@loggman, @alert_queue_manager) # Get database details from environment variables db_details = { @@ -36,7 +41,7 @@ dec_pass = decrypt_string_chacha20(db_details[:password], db_details[:key]) # If any of the necessary details are missing, run the first run setup if db_details.values.any?(&:nil?) @loggman.log_warn('Missing or incomplete configuration. Running first run setup.') - first_run_init = FirstRunInit.new(@loggman, db_manager) + first_run_init = FirstRunInit.new(@loggman, @alert_queue_manager, db_manager) first_run_init.run # Reload environment variables after first run setup Dotenv.load @@ -53,7 +58,7 @@ end # Test connection unless db_manager.test_db_connection(db_details[:username], dec_pass, db_details[:database]) @loggman.log_warn('Failed to connect to the database with existing configuration. Please re-enter your details.') - first_run_init = FirstRunInit.new(@loggman, db_manager) + first_run_init = FirstRunInit.new(@loggman, @alert_queue_manager, db_manager) first_run_init.run # Reload environment variables after potentially updating config Dotenv.load @@ -76,4 +81,10 @@ else end @loggman.log_warn('Program successfully ran with no errors') + # TODO: Add the rest of application logic here + +# End of the program + +# wait for the alert_queue_manager to block before we exit. +@alert_queue_manager.join_worker diff --git a/lib/utils/alert_manager.rb b/lib/utils/alert_manager.rb new file mode 100644 index 0000000..634c15b --- /dev/null +++ b/lib/utils/alert_manager.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Class for creating and displaying alerts in the Curses TUI. This class also manages a little bit of concurrency +# We use mutex for sync so we don't break Curses, as Curses isn't thread safe +class Alert + attr_reader :message, :severity + + def initialize(message, severity) + @message = message + @severity = severity + @curses_mutex = Mutex.new + end + + def display + @curses_mutex.synchronize do + # Initialize color pairs + Curses.start_color + Curses.init_pair(1, Curses::COLOR_BLUE, Curses::COLOR_BLACK) # Info + Curses.init_pair(2, Curses::COLOR_RED, Curses::COLOR_BLACK) # Error + Curses.init_pair(3, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # Warning + + # Create a new window for the alert at the bottom of the screen + alert_window = Curses::Window.new(1, Curses.cols, Curses.lines - 1, 0) + + # Set the color attribute based on the severity of the alert + case @severity + when :info + alert_window.attron(Curses.color_pair(1) | Curses::A_NORMAL) # Blue color + when :warning + alert_window.attron(Curses.color_pair(3) | Curses::A_NORMAL) # Yellow color + when :error + alert_window.attron(Curses.color_pair(2) | Curses::A_NORMAL) # Red color + end + + # Add the message to the window and refresh it to display the message + alert_window.addstr(@message) + alert_window.refresh + + # Create a new thread to handle the delay and clearing of the alert + # This is done in a separate thread to prevent the entire program from + # pausing while the alert is displayed + Thread.new do + sleep(5) # Pause for 5 seconds + + # Clear the alert + alert_window.clear + alert_window.refresh + alert_window.close + end + end + end +end diff --git a/lib/utils/alert_queue_manager.rb b/lib/utils/alert_queue_manager.rb new file mode 100644 index 0000000..dd093d8 --- /dev/null +++ b/lib/utils/alert_queue_manager.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative 'ring_buffer' +# Class for managing the queues for alerts +class AlertQueueManager + def initialize(logger, size = 2 * 1024 * 1024) + @loggman = logger + @queue = RingBuffer.new(@loggman, size) + + # Start a thread that continuously checks the queue and displays alerts + @worker_thread = Thread.new do + loop do + alert = @queue.pop # This will block until there's an alert in the queue + alert&.display + end + end + end + + def enqueue_alert(alert) + @queue.push(alert) + end + + def join_worker + @worker_thread.join + end +end diff --git a/lib/utils/database_manager.rb b/lib/utils/database_manager.rb index dc1612c..ecbe84e 100644 --- a/lib/utils/database_manager.rb +++ b/lib/utils/database_manager.rb @@ -9,26 +9,29 @@ require_relative '../utils/utilities' class DatabaseManager include Utilities - def initialize(logger) + def initialize(logger, alert_queue_manager) @db = nil @loggman = logger + @alert_queue_manager = alert_queue_manager end def test_db_connection(username, password, database) # rubocop:disable Metrics/MethodLength @loggman.log_info('Attempting to connect to the database...') - display_alert('Attempting to connect to the database...', :info) # Create the connection string connection_string = "mysql2://#{username}:#{password}@localhost/#{database}" @db = Sequel.connect(connection_string) # Try a simple query to test the connection @db.run 'SELECT 1' + @loggman.log_info('Successfully connected to the database.') - display_alert('Successfully connected to the database.', :info) + alert = Alert.new('Successfully connected to the database.', :info) + @alert_queue_manager.enqueue_alert(alert) true rescue Sequel::DatabaseConnectionError => e @loggman.log_error("Failed to connect to the database: #{e.message}") - display_alert('Failed to connect to the database!', :error) + alert = Alert.new('Failed to connect to the database!', :error) + @alert_queue_manager.enqueue_alert(alert) false end @@ -40,7 +43,8 @@ class DatabaseManager database = ENV['DB_DATABASE'] if test_db_connection(username, password, database) @loggman.log_info('Successfully connected to the database.') - display_alert('Successfully connected to the Database1', :info) + alert = Alert.new('Successfully connected to the database.') + @alert_queue_manager.enqueue_alert(alert) else # If the connection attempt fails, log an error and return @loggman.log_error('Failed to connect to the database.') diff --git a/lib/utils/first_run_init.rb b/lib/utils/first_run_init.rb index 87755c0..5e2791a 100644 --- a/lib/utils/first_run_init.rb +++ b/lib/utils/first_run_init.rb @@ -6,17 +6,17 @@ require 'dotenv' require_relative 'database_manager' require_relative 'system_information_gather' require_relative 'utilities' +require_relative 'alert_manager' # first run class class FirstRunInit include Utilities include Curses - def initialize(logger, db_manager = nil) - @db_manager = db_manager || DatabaseManager.new(logger) - @info_gatherer = SystemInformationGather.new(@db_manager, logger) - @loggman = logger - Dotenv.load + def initialize(loggman, alert_queue_manager, db_manager = nil) + @loggman = loggman + @db_manager = db_manager + @alert_queue_manager = alert_queue_manager end def run @@ -62,6 +62,8 @@ class FirstRunInit Curses.setpos(1, 0) Curses.addstr('Please enter your database username: ') Curses.refresh + alert = Alert.new('This is a test alert', :error) + @alert_queue_manager.enqueue_alert(alert) username = DCI.catch_input(true) @loggman.log_info('Database Username entered!') @@ -92,7 +94,7 @@ class FirstRunInit @loggman.log_info('Wiriting Database details to a file!') end - def write_db_details_to_config_file(db_details) + def write_db_details_to_config_file(db_details) # rubocop:disable Metrics/MethodLength # Write the database details to the .env file File.open('.env', 'w') do |file| file.puts %(DB_USERNAME="#{db_details[:username]}") diff --git a/lib/utils/redis_queue.rb b/lib/utils/redis_queue.rb deleted file mode 100644 index 2c54641..0000000 --- a/lib/utils/redis_queue.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Class for managing the worker queue in Redis -class RedisQueueManager - def initialize(logger) - @loggman = logger - end -end diff --git a/lib/utils/ring_buffer.rb b/lib/utils/ring_buffer.rb new file mode 100644 index 0000000..f8725a4 --- /dev/null +++ b/lib/utils/ring_buffer.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Since the Standard Ruby library doesn't come with a Ring Buffer implementation, we need to make our own. +# this class creates a simple and rudementary implementation of a Ring Buffer for us to use. +class RingBuffer + def initialize(logger, size) + @loggman = logger + @size = size + @buffer = Array.new(size) + @start = 0 + @end = 0 + end + + def push(element) + @loggman.log_warn('Attempted to push to a full buffer. Overwriting old data.') if full? + + @buffer[@end] = element + @end = (@end + 1) % @size + @start = (@start + 1) % @size if @end == @start + end + + def pop + if empty? + @loggman.log_warn('Attempted to pop from an empty buffer. Returning nil.') + return nil + end + + element = @buffer[@start] + @start = (@start + 1) % @size + element + end + + def empty? + @start == @end + end + + def full? + (@end + 1) % @size == @start + end +end diff --git a/lib/utils/utilities.rb b/lib/utils/utilities.rb index 2ce7838..037fa95 100644 --- a/lib/utils/utilities.rb +++ b/lib/utils/utilities.rb @@ -75,34 +75,6 @@ module Utilities nil end - def display_alert(message, severity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - case severity - when :info - Curses.attron(Curses.color_pair(1)) # Blue color - when :warning - Curses.attron(Curses.color_pair(3)) # Yellow color - when :error - Curses.attron(Curses.color_pair(2)) # Red color - end - - Curses.setpos(Curses.lines - 1, 0) - Curses.addstr(message) - Curses.refresh - - Thread.new do - sleep(5) # Pause for 5 seconds - - # Clear the alert - Curses.setpos(Curses.lines - 1, 0) - Curses.clrtoeol - Curses.refresh - end - - Curses.attroff(Curses.color_pair(1)) if severity == :info - Curses.attroff(Curses.color_pair(3)) if severity == :warning - Curses.attroff(Curses.color_pair(2)) if severity == :error - end - def ask_for_sudo(logger) @loggman = logger @loggman.log_info('Asking for sudo password...')