Class: AgentsControl::Hooks::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/agents_control/hooks/server.rb

Overview

Receiver for events from agents.

HTTP over bare sockets, not webrick: it left the stdlib in Ruby 3, and pulling in a gem just to accept local POSTs isn't worth it. The subset of the protocol needed — one request line, headers, a body — fits in about fifty lines.

Listens strictly on 127.0.0.1. The port must never be exposed externally under any circumstances: anyone who could reach it could grant the agent permission to run commands.

The handler holds the connection open until a human answers — this is exactly how a hook blocks the agent. So every connection gets its own thread, and there can be as many as there are sessions waiting at once.

Defined Under Namespace

Classes: PortBusy

Constant Summary collapse

HOST =
"127.0.0.1"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(port: 0, secret: nil, logger: nil) ⇒ Server

Returns a new instance of Server.



30
31
32
33
34
35
# File 'lib/agents_control/hooks/server.rb', line 30

def initialize(port: 0, secret: nil, logger: nil)
  @requested_port = port
  @secret = secret || SecureRandom.hex(16)
  @logger = logger
  @running = false
end

Instance Attribute Details

#portObject (readonly)

Returns the value of attribute port.



28
29
30
# File 'lib/agents_control/hooks/server.rb', line 28

def port
  @port
end

#secretObject (readonly)

Returns the value of attribute secret.



28
29
30
# File 'lib/agents_control/hooks/server.rb', line 28

def secret
  @secret
end

Instance Method Details

#occupant_hintObject

"Something's already running somewhere" with no indication of where is a useless message: finding the process becomes a manual hunt. Name it right away.



59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/agents_control/hooks/server.rb', line 59

def occupant_hint
  # argv array, not a shell string: the port ultimately comes from
  # the user's own config, but there's no reason to trust a shell
  # to parse it correctly either.
  out, = Open3.capture3("lsof", "-nP", "-iTCP:#{@requested_port.to_i}", "-sTCP:LISTEN", "-t")
  pids = out.split.map(&:strip)
  return "" if pids.empty?

  " by process #{pids.join(', ')}. Stop it: agents_control stop"
rescue StandardError
  ""
end

#start(&handler) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/agents_control/hooks/server.rb', line 42

def start(&handler)
  @server = TCPServer.new(HOST, @requested_port)
  @port = @server.addr[1]
  @running = true

  @thread = Thread.new do
    accept_loop(&handler)
  end

  self
rescue Errno::EADDRINUSE
  raise PortBusy, "port #{@requested_port} is taken#{occupant_hint}"
end

#stopObject



74
75
76
77
78
# File 'lib/agents_control/hooks/server.rb', line 74

def stop
  @running = false
  @server&.close
  @thread&.kill
end

#urlObject



72
# File 'lib/agents_control/hooks/server.rb', line 72

def url = "http://#{HOST}:#{port}"

#waitObject



80
# File 'lib/agents_control/hooks/server.rb', line 80

def wait = @thread&.join