class Bot
Constants
- ACTIONS
- CONSOLE_SYNC_FILE
- DEFAULT_LOG_LINES
- HELP_ALIASES
- LIST_ALIASES
- MAX_LOG_LINES
- MUTATING_ACTIONS
- RABBOT_ROOT
- STYLE_TITLE
- TAG
Attributes
Public Class Methods
Source
# File plugins/bot.rb, line 466 def admin_allowed?(network:, channel:, nick:, is_op:) ChannelTrust.human_admin_nick?( network: network, channel: channel, nick: nick, is_op: is_op ) end
Source
# File plugins/bot.rb, line 353 def apply_runtime_snapshot(snapshot, self_bot_id:, pid:) return snapshot unless self_bot_id && snapshot[:id].to_s == self_bot_id.to_s snapshot.merge(state: :running, pid: pid, enabled: true) end
Source
# File plugins/bot.rb, line 135 def bot_nicks(cinch_bot) ([cinch_bot.nick, cinch_bot.config.nick] + Array(cinch_bot.config.nicks)) .compact .map { |n| n.to_s.strip } .reject(&:empty?) .uniq end
Source
# File plugins/bot.rb, line 235 def console_supervisor_pid(root: RABBOT_ROOT, deps: {}) return deps[:supervisor_pid] if deps.key?(:supervisor_pid) find_supervisor_pid(proc_table: deps[:proc_table] || default_proc_table) end
Source
# File plugins/bot.rb, line 198 def default_proc_table table = {} Dir.glob("/proc/[0-9]*/cmdline").each do |path| pid = File.basename(File.dirname(path)).to_i table[pid] = File.read(path).split("\0") rescue Errno::ENOENT, Errno::EACCES next end table end
Source
# File plugins/bot.rb, line 475 def denial_message "Only registered human admins can manage bots (bots cannot control the fleet)." end
Source
# File plugins/bot.rb, line 178 def disabled_bot_ids(root: RABBOT_ROOT) registry = BotConsole::DisabledRegistry.new(root) BotConsole::Config.discover_bots(root) .map { |entry| entry[:id] } .select { |id| registry.disabled?(id) } end
Source
# File plugins/bot.rb, line 253 def discover_live_bots(root, proc_table: default_proc_table) entries = BotConsole::Config.discover_bots(root) live = {} proc_table.each do |pid, cmdline| next unless cmdline.include?("rabbot.rb") config_index = cmdline.index("rabbot.rb") next unless config_index config = cmdline[config_index + 1].to_s next if config.empty? entry = entries.find do |item| config == item[:rel_config] || config.end_with?("/#{item[:rel_config]}") end live[entry[:id]] = pid if entry end live end
Source
# File plugins/bot.rb, line 479 def dispatch(manager, parsed, runtime: nil, deps: {}) refresh_enabled_states(manager) hydrate_running_states!(manager, deps: deps) action = parsed[:action] return help_message if action == :help return usage_message if action == :unknown return format_list(snapshots(manager, runtime: runtime), disabled_count: manager.disabled_bots.length) if action == :list if action == :status return usage_message if parsed[:target].to_s.empty? bot = find_bot(manager, parsed[:target]) return unknown_bot_message(parsed[:target]) unless bot return format_status(snapshot_for_bot(bot, runtime: runtime)) end if action == :logs return usage_message if parsed[:target].to_s.empty? bot = find_bot(manager, parsed[:target]) return unknown_bot_message(parsed[:target]) unless bot return format_logs(snapshot_from(bot), lines: parsed[:lines] || DEFAULT_LOG_LINES) end dispatch_action(manager, action, parsed, runtime: runtime, deps: deps) end
Source
# File plugins/bot.rb, line 508 def dispatch_action(manager, action, parsed, runtime: nil, deps: {}) target = parsed[:target] return usage_message if target.to_s.empty? if target.to_s.casecmp("all").zero? return dispatch_all(manager, action) end bot = find_bot(manager, target) return unknown_bot_message(target) unless bot start = parsed[:action] == :enable ? parsed.fetch(:start, true) : parsed[:start] detail = run_bot_action(manager, bot, action, start: start, deps: deps) return detail if detail.is_a?(String) && detail.start_with?("Usage", "Unknown", "Cannot") refresh_enabled_states(manager) hydrate_running_states!(manager, deps: deps) format_action_result(action, snapshot_for_bot(bot, runtime: runtime), detail: detail) end
Source
# File plugins/bot.rb, line 528 def dispatch_all(manager, action) case action when :start manager.start_all_stopped_enabled "started all stopped enabled bots" when :restart manager.restart_all_enabled "restarted all enabled bots" when :reload manager.reload_plugins_all_enabled "reload signaled for all running enabled bots" else "Usage: !bot #{action} <id|all>" end end
Source
# File plugins/bot.rb, line 121 def find_bot(manager, target) text = target.to_s.strip return nil if text.empty? return :all if text.casecmp("all").zero? found = manager.find(text) return found if found normalized = NormalizeBotId.call(text) manager.bots.find do |bot| bot.id == normalized || bot.name.to_s.casecmp(text).zero? end end
Source
# File plugins/bot.rb, line 216 def find_supervisor_pid(proc_table: default_proc_table, start_pid: Process.ppid) pid = start_pid 10.times do break if pid.nil? || pid <= 1 cmdline = proc_table[pid] || begin File.read("/proc/#{pid}/cmdline").split("\0") rescue Errno::ENOENT, Errno::EACCES nil end if cmdline joined = cmdline.join(" ") return pid if joined.include?("bot_console") end pid = proc_ppid(pid, proc_table: proc_table) end nil end
Source
# File plugins/bot.rb, line 436 def format_action_result(action, snapshot, detail: nil) label = [action, snapshot[:id], detail].compact.join(" ") [ IrcFormat.report_header(tag: TAG, title: label, style: STYLE_TITLE), format_state_line(snapshot), IrcFormat.report_footer("bot manager") ].join("\n") end
Source
# File plugins/bot.rb, line 394 def format_list(snapshots, disabled_count: 0) enabled = snapshots.count { |row| row[:enabled] } title = "Rabbot fleet · #{snapshots.length} bots · #{enabled} enabled" lines = [ IrcFormat.report_header(tag: TAG, title: title, style: STYLE_TITLE), *snapshots.map { |row| format_state_line(row) }, IrcFormat.report_footer("disabled #{disabled_count}", "!bot help for commands") ] lines.join("\n") end
Source
# File plugins/bot.rb, line 425 def format_logs(snapshot, lines: DEFAULT_LOG_LINES) tail = Array(snapshot[:logs]).last(lines) body = tail.empty? ? "(no log lines)" : tail.join("\n") title = "#{snapshot[:id]} · last #{lines} lines" [ IrcFormat.report_header(tag: TAG, title: title, style: STYLE_TITLE), body, IrcFormat.report_footer("bot console log") ].join("\n") end
Source
# File plugins/bot.rb, line 386 def format_state_line(snapshot) parts = [snapshot[:id].to_s, snapshot[:state].to_s] parts << "pid #{snapshot[:pid]}" if snapshot[:pid] parts << "disabled" unless snapshot[:enabled] parts << snapshot[:level].to_s unless snapshot[:level].to_s.empty? parts.join(" · ") end
Source
# File plugins/bot.rb, line 405 def format_status(snapshot) meta = [ snapshot[:name].to_s, snapshot[:state].to_s, ("pid #{snapshot[:pid]}" if snapshot[:pid]), ("disabled" unless snapshot[:enabled]), snapshot[:level].to_s ].compact.join(" · ") body_lines = SearchReport.colon_body_lines([ { label: "id", value: snapshot[:id].to_s }, { label: "config", value: "#{snapshot[:id]}.yml" } ]) [ IrcFormat.report_header(tag: TAG, title: meta, style: STYLE_TITLE), *body_lines ].join("\n") end
Source
# File plugins/bot.rb, line 449 def help_message [ IrcFormat.report_header(tag: TAG, title: "Bot console commands", style: STYLE_TITLE), "list — show all personas and state", "status <id> — one bot detail", "logs <id> [n] — recent log lines (max #{MAX_LOG_LINES})", "start <id|all> — start stopped bots", "stop <id> — graceful stop (SIGTERM)", "kill <id> — force stop (SIGKILL)", "restart <id|all> — stop then start", "reload <id|all> — plugin reload (USR1)", "enable <id> [only] — re-enable disabled bot (starts by default; only = enable without start)", "disable <id> — stop and hide from console", IrcFormat.report_footer("owner bot · registered admins only") ].join("\n") end
Source
# File plugins/bot.rb, line 280 def hydrate_running_states!(manager, root: RABBOT_ROOT, live_bots: nil, alive_pids: nil, deps: {}) supervised = console_supervisor_pid(root: root, deps: deps) return unless supervised || live_bots proc_table = deps[:proc_table] || default_proc_table live_bots ||= discover_live_bots(root, proc_table: proc_table) manager.bots.each do |bot| pid = live_bots[bot.id] next unless pid next unless alive_pids ? alive_pids.include?(pid) : process_alive?(pid) next if bot.respond_to?(:disabled?) && bot.disabled? bot.instance_variable_set(:@pid, pid) bot.instance_variable_set(:@state, :running) end end
Source
# File plugins/bot.rb, line 45 def manager(root: RABBOT_ROOT) return @manager if @manager @manager_mutex.synchronize do @manager ||= BotConsole::Manager.new( root: root, env: ENV.to_h.merge(BotConsole.load_secrets_env(root)) ) end end
Source
# File plugins/bot.rb, line 297 def mirror_console_enable(tty:, root:, target:, start:) index = disabled_bot_ids(root: root).index(target.to_s) return false unless index && tty tty.write("m") index.times { tty.write("\e[B") } start ? tty.write("\r") : tty.write("e") true end
Source
# File plugins/bot.rb, line 241 def open_supervisor_tty(supervisor_pid, opener: nil) return nil unless supervisor_pid opener ||= lambda do |pid| fd0 = File.readlink("/proc/#{pid}/fd/0") File.open(fd0, "w") rescue StandardError nil end opener.call(supervisor_pid) end
Source
# File plugins/bot.rb, line 60 def parse_command(args) text = args.to_s.strip return { action: :list } if text.empty? action_token, rest = text.split(/\s+/, 2) action_key = action_token.to_s.downcase action = if LIST_ALIASES.include?(action_key) :list elsif HELP_ALIASES.include?(action_key) :help elsif ACTIONS.include?(action_key.to_sym) action_key.to_sym else return { action: :unknown, raw: text } end case action when :list, :help { action: action } when :logs parse_logs_command(rest) when :enable parse_enable_command(rest) else target = rest.to_s.strip target = nil if target.empty? { action: action, target: target } end end
Source
# File plugins/bot.rb, line 102 def parse_enable_command(rest) parts = rest.to_s.strip.split(/\s+/) return { action: :enable, target: nil, start: true } if parts.empty? tail = parts.last.to_s.downcase start = if tail == "only" false else true end target = if %w[start only].include?(tail) parts[0..-2].join(" ") else parts.join(" ") end target = parts.first if target.to_s.strip.empty? { action: :enable, target: target, start: start } end
Source
# File plugins/bot.rb, line 90 def parse_logs_command(rest) text = rest.to_s.strip return { action: :logs, target: nil } if text.empty? if (match = text.match(/\A(\S+)\s+(\d+)\z/)) lines = match[2].to_i.clamp(1, MAX_LOG_LINES) { action: :logs, target: match[1], lines: lines } else { action: :logs, target: text.split.first, lines: DEFAULT_LOG_LINES } end end
Source
# File plugins/bot.rb, line 209 def proc_ppid(pid, proc_table: default_proc_table) stat = File.read("/proc/#{pid}/stat") stat.split(" ", 5)[3].to_i rescue Errno::ENOENT, Errno::EACCES nil end
Source
# File plugins/bot.rb, line 273 def process_alive?(pid) Process.kill(0, pid) true rescue Errno::ESRCH false end
Source
# File plugins/bot.rb, line 185 def record_console_sync(root:, action:, target:, start: nil) payload = { action: action.to_s, target: target.to_s, start: start, at: Time.now.utc.iso8601 } path = File.join(root, "data", CONSOLE_SYNC_FILE) FileUtils.mkdir_p(File.dirname(path)) File.write(path, JSON.pretty_generate(payload)) payload end
Source
# File plugins/bot.rb, line 165 def refresh_enabled_states(manager, root: RABBOT_ROOT) return unless manager.respond_to?(:registry) registry = BotConsole::DisabledRegistry.new(root) manager.bots.each do |bot| if registry.disabled?(bot.id) bot.disable! unless bot.disabled? elsif bot.disabled? bot.enable! end end end
Source
# File plugins/bot.rb, line 56 def reset_manager! @manager_mutex.synchronize { @manager = nil } end
Source
# File plugins/bot.rb, line 150 def resolve_self_bot_id(root:, nicks:) lookup_nicks = nicks.map { |nick| nick.to_s.strip }.reject(&:empty?) return nil if lookup_nicks.empty? BotConsole::Config.discover_bots(root).each do |entry| raw = YAML.safe_load_file(entry[:config_path], aliases: false) || {} entry_nicks = [raw["nick"], *Array(raw["nicks"])].compact.map { |nick| nick.to_s.strip }.reject(&:empty?) next if entry_nicks.empty? return entry[:id] if entry_nicks.any? { |nick| lookup_nicks.any? { |candidate| candidate.casecmp(nick).zero? } } end nil end
Source
# File plugins/bot.rb, line 544 def run_bot_action(manager, bot, action, start: false, deps: {}) proc_table = deps.fetch(:proc_table, default_proc_table) supervised = console_supervisor_pid(deps: deps.merge(proc_table: proc_table)) live_bots = supervised ? discover_live_bots(RABBOT_ROOT, proc_table: proc_table) : {} live_pid = live_bots[bot.id] case action when :start if live_pid && process_alive?(live_pid) record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) return "already running" end started = bot.start record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) started ? "started" : "not started" when :stop if live_pid && signal_live_bot(:stop, live_pid) record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) return "stopped" end bot.stop record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) "stopped" when :kill if live_pid && signal_live_bot(:kill, live_pid) record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) return "terminated" end bot.terminate record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) "terminated" when :restart if live_pid signal_live_bot(:stop, live_pid) sleep 0.2 else bot.stop end bot.start record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) "restarted" when :reload if live_pid && signal_live_bot(:reload, live_pid) record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) return "reload signaled" end result = bot.reload_plugins ? "reload signaled" : "not running" record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) result when :disable manager.disable_bot(bot.id) record_console_sync(root: RABBOT_ROOT, action: action, target: bot.id) "disabled" when :enable run_enable_action(manager, bot, start: start, deps: deps) else usage_message end end
Source
# File plugins/bot.rb, line 307 def run_enable_action(manager, bot, start:, root: RABBOT_ROOT, deps: {}) registry = BotConsole::DisabledRegistry.new(root) was_disabled = registry.disabled?(bot.id) supervisor_pid = console_supervisor_pid(root: root, deps: deps) if was_disabled && supervisor_pid tty = deps[:open_tty] ? deps[:open_tty].call(supervisor_pid) : open_supervisor_tty(supervisor_pid) if tty && mirror_console_enable(tty: tty, root: root, target: bot.id, start: start) record_console_sync(root: root, action: :enable, target: bot.id, start: start) refresh_enabled_states(manager, root: root) hydrate_running_states!(manager, root: root, deps: deps) return start ? "enabled and started" : "enabled" end end manager.enable_bot(bot.id, start: start) record_console_sync(root: root, action: :enable, target: bot.id, start: start) refresh_enabled_states(manager, root: root) hydrate_running_states!(manager, root: root, deps: deps) start ? "enabled and started" : "enabled" end
Source
# File plugins/bot.rb, line 143 def runtime_context(cinch_bot) { self_bot_id: resolve_self_bot_id(root: RABBOT_ROOT, nicks: bot_nicks(cinch_bot)), pid: Process.pid } end
Source
# File plugins/bot.rb, line 329 def signal_live_bot(action, pid) case action when :stop begin Process.kill("TERM", -pid) rescue Errno::ESRCH Process.kill("TERM", pid) end when :kill begin Process.kill("KILL", -pid) rescue Errno::ESRCH Process.kill("KILL", pid) end when :reload Process.kill("USR1", pid) else return false end true rescue Errno::ESRCH false end
Source
# File plugins/bot.rb, line 359 def snapshot_for_bot(bot, runtime: nil) snapshot = snapshot_from(bot) return snapshot unless runtime apply_runtime_snapshot( snapshot, self_bot_id: runtime[:self_bot_id], pid: runtime[:pid] ) end
Source
# File plugins/bot.rb, line 374 def snapshot_from(bot) { id: bot.id, name: bot.name, state: bot.state, pid: bot.pid, enabled: bot.enabled?, level: bot.level, logs: bot.buffer.lines } end
Source
# File plugins/bot.rb, line 370 def snapshots(manager, runtime: nil) manager.bots.map { |bot| snapshot_for_bot(bot, runtime: runtime) } end
Source
# File plugins/bot.rb, line 608 def unknown_bot_message(target) "Unknown bot: #{target}. Try !bot list." end
Source
# File plugins/bot.rb, line 445 def usage_message "Usage: !bot list|status <id>|logs <id> [n]|start <id|all>|stop <id>|restart <id|all>|reload <id|all>|enable <id> [only]|disable <id>|kill <id>" end
Public Instance Methods
Source
# File plugins/bot.rb, line 760 def execute(m, args) return unless BotIdentity.owner_bot?(bot) parsed = self.class.parse_command(args) unless self.class.admin_allowed?( network: bot.config.server, channel: m.channel&.name, nick: m.user.nick, is_op: m.channel ? m.channel.opped?(m.user) : false ) return themed_flood_safe_reply(m, self.class.denial_message) end result = self.class.dispatch(self.class.manager, parsed, runtime: self.class.runtime_context(bot)) themed_flood_safe_reply(m, result) end