NAME
Fugu::Control, Fugu::Control::Client - a control socket for a running daemon
SYNOPSIS
# In the daemon
use Fugu::Control;
my $control = Fugu::Control->new(path => '/var/run/myd/control.sock');
$control->register(status => sub ($args) { { paired => 1 } });
$control->register(devices => sub ($args) { [ @devices ] });
$control->listen(loop => $loop) or die $control->error;
# and at shutdown
$control->shutdown(loop => $loop);
# In the tool
my $client = Fugu::Control::Client->new(
path => '/var/run/myd/control.sock');
my $status = $client->request('status');
unless ($status) {
die "the daemon is not running\n" if $client->socket_absent;
die $client->error . "\n";
}
DESCRIPTION
A tool that reports on a daemon has two ways to get its answer. It can guess from files on disk, or it can ask the daemon. The first way reads what the daemon wrote at some earlier time, and it is wrong whenever the two disagree. Fugu::Control is the second way.
The server registers with a Fugu::EventLoop and answers commands from process state. The client connects, sends one request, and reads the reply. The payload is JSON, and the transport is the imsg(3) framing of Fugu::Imsg.
Everything that arrives on the socket is untrusted. An unknown command, a payload that is not JSON, and a frame over the limit all give an error reply. None of them ends the daemon.
The server
new(%args)-
Make a server. The method opens nothing.
pathis the socket, and it is necessary.logis the logger, and it defaults to the process default of Fugu::Log. register($command, $code)-
Add a command. The code gets the decoded arguments hash reference and returns the reply, which must encode as JSON. A handler that dies gives the caller an error reply.
listen(%args)-
Bind the socket and start accepting. The listener and every accepted connection register with the event loop that
loopnames. That argument is necessary, and the method dies without it. accept_one($loop)-
Take one connection and register it as a read handler on the loop.
shutdown(%args)-
Close every connection, close the listener, and remove the socket. A socket left behind names a daemon that is not there.
path()-
The socket path.
error()-
The most recent failure.
The client
new(%args)-
Make a client.
pathis necessary;timeoutis the per-frame deadline and defaults to 5 seconds. request($command, $args)-
Send one command and return the decoded reply, or
undefwith the reason inerror(). The method connects if it is not connected. connect(),disconnect()-
Open and close the connection by hand. Both are idempotent.
socket_absent()-
Report if the most recent failure was an absent socket, and not a refusal. A tool says "the daemon is not running" for the first and "the daemon said no" for the second. A tool that cannot tell them apart reports the wrong thing to an operator half the time.
A socket inside a directory that the caller may not search is not an absent socket.
connect()tells the two apart and reports the permission, because an operator who reads "not running" about a daemon that is running looks in the wrong place. error()-
The most recent failure.
RETURN VALUES
listen() returns the server object, or undef with the reason in error(). request() returns the decoded reply, or undef. socket_absent() returns 1 or 0. Every other method returns its object.
FILES
The socket that the caller names. The server creates it mode 0600, under a umask(2) guard, so it is never world-reachable even for an instant. A chmod(2) after the bind would leave that window open.
The mode of the socket is the inner boundary. The directory that holds it is the outer one, and the caller owns that. A daemon that drops privileges creates the directory while it is still root, mode 0700, owned by the daemon user.
ERRORS
new() dies without a path, and register() dies without a code reference. Those are programming errors.
Nothing that arrives on the socket makes anything die. A malformed request, an unknown command, and a handler that died all give an error reply, and the connection stays usable.
listen() refuses to take a socket that another process answers on, and replaces one that nothing is behind. bind(2) fails on an existing name, and a daemon that will not start after a crash needs a hand at every reboot.
SEE ALSO
imsg(3), Fugu::EventLoop, Fugu::Imsg, Fugu::Log
AUTHORS
Dick Olsson <hi@senzilla.io>
CAVEATS
The socket carries no secret. That is a rule for the caller, which writes the command handlers, and this module cannot enforce it. A daemon whose configuration holds a setup code and a broker password must not answer a command that echoes them back: the reply lands in the terminal of an operator who may not be alone.
One imsg frame carries about 16 KB. A larger reply spans several frames and the client puts them back together, up to one megabyte in total. A command that would answer with more than that gets an error instead.
A handler runs inside the event loop of the daemon. A handler that blocks blocks the daemon.