<commit_before>require "io/console"
require 'pty'

module Invoker
  class Commander
    MAX_PROCESS_COUNT = 10
    LABEL_COLORS = ['green', 'yellow', 'blue', 'magenta', 'cyan']
    attr_accessor :reactor, :workers, :thread_group, :open_pipes
    
    def initialize
      # mapping between open pipes and worker classes
      @open_pipes = {}

      # mapping between command label and worker classes
      @workers = {}
      
      @thread_group = ThreadGroup.new()
      @worker_mutex = Mutex.new()
      @reactor = Invoker::Reactor.new
      Thread.abort_on_exception = true
    end

    def start_manager
      if !Invoker::CONFIG.processes || Invoker::CONFIG.processes.empty?
        raise Invoker::Errors::InvalidConfig.new("No processes configured in config file")
      end
      install_interrupt_handler()
      unix_server_thread = Thread.new { Invoker::CommandListener::Server.new() }
      thread_group.add(unix_server_thread)
      Invoker::CONFIG.processes.each { |process_info| add_command(process_info) }
      reactor.start
    end

    def add_command(process_info)
      m, s = PTY.open
      s.raw! # disable newline conversion.

      pid = run_command(process_info, s)

      s.close()

      selected_color = LABEL_COLORS.shift()
      LABEL_COLORS.push(selected_color)
      worker = Invoker::CommandWorker.new(process_info.label, m, pid, selected_color)

      add_worker(worker)
      wait_on_pid(process_info.label,pid)
    end

    def add_command_by_label(command_label)
      process_info = Invoker::CONFIG.processes.detect {|pconfig|
        pconfig.label == command_label
      }
      if process_info
        add_command(process_info)
      end
    end

    def reload_command(command_label)
      remove_command(command_label)
      add_command_by_label(command_label)
    end

    def remove_command(command_label, rest_args)
      worker = workers[command_label]
      signal_to_use = rest_args ? Array(rest_args).first : 'INT'

      if worker
        $stdout.puts("Removing #{command_label} with signal #{signal_to_use}".red)
        process_kill(worker.pid, signal_to_use)
      end
    end

    def get_worker_from_fd(fd)
      open_pipes[fd.fileno]
    end

    def get_worker_from_label(label)
      workers[label]
    end
    
    private
    def process_kill(pid, signal_to_use)
      if signal_to_use.to_i == 0
        Process.kill(signal_to_use, pid)
      else
        Process.kill(signal_to_use.to_i, pid)
      end
    end
    
    # Remove worker from all collections
    def remove_worker(command_label)
      @worker_mutex.synchronize do
        worker = @workers[command_label]
        if worker
          @open_pipes.delete(worker.pipe_end.fileno)
          @reactor.remove_from_monitoring(worker.pipe_end)
          @workers.delete(command_label)
        end
      end
    end

    # add worker to global collections
    def add_worker(worker)
      @worker_mutex.synchronize do
        @open_pipes[worker.pipe_end.fileno] = worker
        @workers[worker.command_label] = worker
        @reactor.add_to_monitor(worker.pipe_end)
      end
    end

    def run_command(process_info, write_pipe)
      if defined?(Bundler)
        Bundler.with_clean_env do
          spawn(process_info.cmd, 
            :chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
          )
        end
      else
        spawn(process_info.cmd, 
          :chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
        )
      end
    end

    def wait_on_pid(command_label,pid)
      raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
      thread = Thread.new do
        Process.wait(pid)
        message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
        $stdout.puts("\n#{message}".red)
        notify_user(message)
        remove_worker(command_label)
      end
      @thread_group.add(thread)
    end

    def notify_user(message)
      if defined?(Bundler)
        Bundler.with_clean_env do
          check_and_notify_with_terminal_notifier(message)
        end
      else
        check_and_notify_with_terminal_notifier(message)
      end
    end

    def check_and_notify_with_terminal_notifier(message)
      return unless RUBY_PLATFORM.downcase.include?("darwin")

      command_path = `which terminal-notifier`
      if command_path && !command_path.empty?
        system("terminal-notifier -message '#{message}' -title Invoker")
      end
    end

    def install_interrupt_handler
      Signal.trap("INT") do
        @workers.each {|key,worker| Process.kill("INT", worker.pid) }
        exit(0)
      end
    end

  end
end<commit_msg>Handle the exception if process is not running<commit_after>require "io/console"
require 'pty'

module Invoker
  class Commander
    MAX_PROCESS_COUNT = 10
    LABEL_COLORS = ['green', 'yellow', 'blue', 'magenta', 'cyan']
    attr_accessor :reactor, :workers, :thread_group, :open_pipes
    
    def initialize
      # mapping between open pipes and worker classes
      @open_pipes = {}

      # mapping between command label and worker classes
      @workers = {}
      
      @thread_group = ThreadGroup.new()
      @worker_mutex = Mutex.new()
      @reactor = Invoker::Reactor.new
      Thread.abort_on_exception = true
    end

    def start_manager
      if !Invoker::CONFIG.processes || Invoker::CONFIG.processes.empty?
        raise Invoker::Errors::InvalidConfig.new("No processes configured in config file")
      end
      install_interrupt_handler()
      unix_server_thread = Thread.new { Invoker::CommandListener::Server.new() }
      thread_group.add(unix_server_thread)
      Invoker::CONFIG.processes.each { |process_info| add_command(process_info) }
      reactor.start
    end

    def add_command(process_info)
      m, s = PTY.open
      s.raw! # disable newline conversion.

      pid = run_command(process_info, s)

      s.close()

      selected_color = LABEL_COLORS.shift()
      LABEL_COLORS.push(selected_color)
      worker = Invoker::CommandWorker.new(process_info.label, m, pid, selected_color)

      add_worker(worker)
      wait_on_pid(process_info.label,pid)
    end

    def add_command_by_label(command_label)
      process_info = Invoker::CONFIG.processes.detect {|pconfig|
        pconfig.label == command_label
      }
      if process_info
        add_command(process_info)
      end
    end

    def reload_command(command_label)
      remove_command(command_label)
      add_command_by_label(command_label)
    end

    def remove_command(command_label, rest_args)
      worker = workers[command_label]
      signal_to_use = rest_args ? Array(rest_args).first : 'INT'

      if worker
        $stdout.puts("Removing #{command_label} with signal #{signal_to_use}".red)
        process_kill(worker.pid, signal_to_use)
      end
    end

    def get_worker_from_fd(fd)
      open_pipes[fd.fileno]
    end

    def get_worker_from_label(label)
      workers[label]
    end
    
    private
    def process_kill(pid, signal_to_use)
      if signal_to_use.to_i == 0
        Process.kill(signal_to_use, pid)
      else
        Process.kill(signal_to_use.to_i, pid)
      end
    end
    
    # Remove worker from all collections
    def remove_worker(command_label)
      @worker_mutex.synchronize do
        worker = @workers[command_label]
        if worker
          @open_pipes.delete(worker.pipe_end.fileno)
          @reactor.remove_from_monitoring(worker.pipe_end)
          @workers.delete(command_label)
        end
      end
    end

    # add worker to global collections
    def add_worker(worker)
      @worker_mutex.synchronize do
        @open_pipes[worker.pipe_end.fileno] = worker
        @workers[worker.command_label] = worker
        @reactor.add_to_monitor(worker.pipe_end)
      end
    end

    def run_command(process_info, write_pipe)
      if defined?(Bundler)
        Bundler.with_clean_env do
          spawn(process_info.cmd, 
            :chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
          )
        end
      else
        spawn(process_info.cmd, 
          :chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
        )
      end
    end

    def wait_on_pid(command_label,pid)
      raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
      thread = Thread.new do
        Process.wait(pid)
        message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
        $stdout.puts("\n#{message}".red)
        notify_user(message)
        remove_worker(command_label)
      end
      @thread_group.add(thread)
    end

    def notify_user(message)
      if defined?(Bundler)
        Bundler.with_clean_env do
          check_and_notify_with_terminal_notifier(message)
        end
      else
        check_and_notify_with_terminal_notifier(message)
      end
    end

    def check_and_notify_with_terminal_notifier(message)
      return unless RUBY_PLATFORM.downcase.include?("darwin")

      command_path = `which terminal-notifier`
      if command_path && !command_path.empty?
        system("terminal-notifier -message '#{message}' -title Invoker")
      end
    end

    def install_interrupt_handler
      Signal.trap("INT") do
        @workers.each {|key,worker|
          begin
            Process.kill("INT", worker.pid) 
          rescue Errno::ESRCH
          end
        }
        exit(0)
      end
    end

  end
end
{'fraction_non_alphanumeric': 0.08971923158116951, 'fraction_numerical': 0.0008444162972345366, 'mean_word_length': 2.634445723053318, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3273867', 'n_tokens_mistral': 2998, 'n_tokens_neox': 2939, 'n_words': 574}