Clean shutdown, stability
This commit is contained in:
+277
-94
@@ -1,18 +1,18 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. Davies
|
||||
*
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "AvahiService.hpp"
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <avahi-client/client.h>
|
||||
#include <avahi-client/publish.h>
|
||||
#include <avahi-common/alternative.h>
|
||||
@@ -35,6 +36,7 @@
|
||||
#include <unistd.h>
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ss.hpp"
|
||||
#include <chrono>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -43,43 +45,122 @@ void AvahiService::Announce(
|
||||
const std::string &name,
|
||||
const std::string &instanceId,
|
||||
const std::string &mdnsName,
|
||||
bool addTestGroup)
|
||||
bool wait)
|
||||
{
|
||||
if (terminated)
|
||||
return;
|
||||
// We should update an existing group if the name doesn't change.
|
||||
// but in practice, the name is the only thing that's going to change without restarting the server.
|
||||
if (this->name && strcmp(name.c_str(),this->name) == 0)
|
||||
// but in practice, the name is the only thing that's going to change without restarting the pipedal server.
|
||||
if (this->serviceName == name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Unannounce();
|
||||
|
||||
this->portNumber = portNumber;
|
||||
this->instanceId = instanceId;
|
||||
this->mdnsName = mdnsName;
|
||||
this->addTestGroup = addTestGroup;
|
||||
|
||||
this->name = avahi_strdup(name.c_str());
|
||||
|
||||
Start();
|
||||
}
|
||||
|
||||
void AvahiService::Unannounce()
|
||||
{
|
||||
Stop();
|
||||
if (name)
|
||||
// this->name = avahi_strdup(name.c_str());
|
||||
if (!started)
|
||||
{
|
||||
avahi_free(name);
|
||||
this->name = nullptr;
|
||||
this->portNumber = portNumber;
|
||||
this->instanceId = instanceId;
|
||||
this->mdnsName = mdnsName;
|
||||
this->serviceName = name;
|
||||
|
||||
started = true;
|
||||
makeAnnouncement = true;
|
||||
createPending = true;
|
||||
Start();
|
||||
if (wait) {
|
||||
Wait();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// already started, so we have to use poll_lock instead.
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
|
||||
// all state that we have to protect with the lock now that the avahi serv3ce is running.
|
||||
this->portNumber = portNumber;
|
||||
this->instanceId = instanceId;
|
||||
this->mdnsName = mdnsName;
|
||||
this->serviceName = name;
|
||||
this->makeAnnouncement = true;
|
||||
|
||||
// we've requested a start but have not created the group.
|
||||
if (this->createPending)
|
||||
{
|
||||
// the first create will use our freshly updated parameters.
|
||||
// no action required.
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise we have to use a lock to effect the update.
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_reset(group); // unannounce the previous
|
||||
SetState(ServiceState::Reset);
|
||||
create_group(client);
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error("Failed to update mDNS service because of a synch problem.");
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
if (wait)
|
||||
{
|
||||
Wait();
|
||||
}
|
||||
}
|
||||
this->group = nullptr;
|
||||
this->client = nullptr;
|
||||
this->threadedPoll = nullptr;
|
||||
}
|
||||
|
||||
void AvahiService::Unannounce(bool wait)
|
||||
{
|
||||
if (terminated)
|
||||
return;
|
||||
if (this->threadedPoll)
|
||||
{
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (started)
|
||||
{
|
||||
if (this->createPending)
|
||||
{
|
||||
// if service is starting up, and we haven't yet made our first announcement,
|
||||
// just signal that we don't want to announc.
|
||||
this->makeAnnouncement = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->makeAnnouncement = false;
|
||||
// we have previously made a successful announcement. Retract it.
|
||||
if (group)
|
||||
{
|
||||
int rc = avahi_entry_group_reset(group);
|
||||
if (rc < 0)
|
||||
{
|
||||
Lv2Log::error("Avahi: failed to un-announce.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
if (wait)
|
||||
{
|
||||
WaitForUnannounce();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void AvahiService::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userData)
|
||||
{
|
||||
((AvahiService *)userData)->EntryGroupCallback(g, state);
|
||||
}
|
||||
|
||||
void AvahiService::SetState(ServiceState serviceState)
|
||||
{
|
||||
this->serviceState = serviceState;
|
||||
}
|
||||
|
||||
void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState state)
|
||||
{
|
||||
group = g;
|
||||
@@ -87,29 +168,39 @@ void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState s
|
||||
switch (state)
|
||||
{
|
||||
case AVAHI_ENTRY_GROUP_ESTABLISHED:
|
||||
SetState(ServiceState::Established);
|
||||
Lv2Log::info(SS("DNS/SD group established."));
|
||||
|
||||
/* The entry group has been established successfully */
|
||||
Lv2Log::debug(SS("Service " << name << " successfully established."));
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_COLLISION:
|
||||
{
|
||||
char *n;
|
||||
/* A service name collision with a remote service
|
||||
* happened. Let's pick a new name */
|
||||
n = avahi_alternative_service_name(name);
|
||||
avahi_free(name);
|
||||
name = n;
|
||||
Lv2Log::error(SS("Service name collision, renaming service to '" << name << "'\n"));
|
||||
* happened. Let's pick a new name */
|
||||
SetState(ServiceState::Collision);
|
||||
char *n = avahi_alternative_service_name(avahiNameString);
|
||||
avahi_free(avahiNameString);
|
||||
avahiNameString = n;
|
||||
Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'\n"));
|
||||
/* And recreate the services */
|
||||
create_group(avahi_entry_group_get_client(g));
|
||||
break;
|
||||
}
|
||||
case AVAHI_ENTRY_GROUP_FAILURE:
|
||||
Lv2Log::error(SS("Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) << "\n"));
|
||||
Lv2Log::error(SS("DNS/SD service shutting down."));
|
||||
/* Some kind of failure happened while we were registering our services */
|
||||
terminated = true;
|
||||
avahi_threaded_poll_quit(threadedPoll);
|
||||
SetState(ServiceState::Failed);
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_UNCOMMITED:
|
||||
SetState(ServiceState::Uncommited);
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_REGISTERING:;
|
||||
SetState(ServiceState::Registering);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +233,14 @@ static int toRawDns(char *rawResult, size_t size, const std::string &name)
|
||||
}
|
||||
void AvahiService::create_group(AvahiClient *c)
|
||||
{
|
||||
this->createPending = false;
|
||||
|
||||
char *n;
|
||||
int ret;
|
||||
assert(c);
|
||||
/* If this is the first time we're called, let's create a new
|
||||
* entry group if necessary */
|
||||
SetState(ServiceState::Requested);
|
||||
if (!group)
|
||||
{
|
||||
if (!(group = avahi_entry_group_new(c, entry_group_callback, (void *)this)))
|
||||
@@ -157,20 +251,26 @@ void AvahiService::create_group(AvahiClient *c)
|
||||
}
|
||||
/* If the group is empty (either because it was just created, or
|
||||
* because it was reset previously, add our entries. */
|
||||
if (avahi_entry_group_is_empty(group))
|
||||
if (this->makeAnnouncement && avahi_entry_group_is_empty(group))
|
||||
{
|
||||
Lv2Log::debug(SS("Adding service '" << name << "'"));
|
||||
Lv2Log::debug(SS("Adding service '" << avahiNameString << "'"));
|
||||
|
||||
std::string instanceTxtRecord = SS("id=" << this->instanceId);
|
||||
|
||||
#define PIPEDAL_SERVICE_TYPE "_pipedal._tcp"
|
||||
|
||||
if (avahiNameString)
|
||||
{
|
||||
avahi_free(avahiNameString);
|
||||
avahiNameString = nullptr;
|
||||
}
|
||||
avahiNameString = avahi_strdup(serviceName.c_str());
|
||||
if ((ret = avahi_entry_group_add_service(
|
||||
group,
|
||||
AVAHI_IF_UNSPEC,
|
||||
AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.)
|
||||
(AvahiPublishFlags)0,
|
||||
name,
|
||||
avahiNameString,
|
||||
PIPEDAL_SERVICE_TYPE,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -186,55 +286,31 @@ void AvahiService::create_group(AvahiClient *c)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (this->addTestGroup)
|
||||
{
|
||||
Lv2Log::info("Added tests DNS/SD service.");
|
||||
std::string instanceTxtRecord = SS("id=" << "0a6045b0-1753-4104-b3e4-b9713b9cc360");
|
||||
|
||||
if ((ret = avahi_entry_group_add_service(
|
||||
group,
|
||||
AVAHI_IF_UNSPEC,
|
||||
AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.)
|
||||
(AvahiPublishFlags)0,
|
||||
"Ed's PiPedal",
|
||||
PIPEDAL_SERVICE_TYPE,
|
||||
NULL,
|
||||
NULL,
|
||||
portNumber,
|
||||
|
||||
// txt records.
|
||||
instanceTxtRecord.c_str(),
|
||||
NULL)) < 0)
|
||||
{
|
||||
if (ret == AVAHI_ERR_COLLISION)
|
||||
goto collision;
|
||||
Lv2Log::error(SS("Failed to add _pipedal._tcp service: " << avahi_strerror(ret)));
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tell the server to register the service */
|
||||
if ((ret = avahi_entry_group_commit(group)) < 0)
|
||||
{
|
||||
Lv2Log::error(SS("Failed to commit entry group: " << avahi_strerror(ret)));
|
||||
goto fail;
|
||||
}
|
||||
Lv2Log::info(SS("DNS/SD service announced."));
|
||||
|
||||
Lv2Log::info(SS("DNS/SD service announced. (" << this->avahiNameString << ")"));
|
||||
}
|
||||
return;
|
||||
collision:
|
||||
/* A service name collision with a local service happened. Let's
|
||||
* pick a new name */
|
||||
n = avahi_alternative_service_name(name);
|
||||
avahi_free(name);
|
||||
name = n;
|
||||
Lv2Log::warning(SS("Service name collision, renaming service to '" << name << "'"));
|
||||
* pick a new avahiNameString */
|
||||
n = avahi_alternative_service_name(avahiNameString);
|
||||
avahi_free(avahiNameString);
|
||||
avahiNameString = n;
|
||||
Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'"));
|
||||
avahi_entry_group_reset(group);
|
||||
create_group(c);
|
||||
return;
|
||||
fail:
|
||||
terminated = true;
|
||||
avahi_threaded_poll_quit(threadedPoll);
|
||||
Lv2Log::error("DNS/SD service shutting down.");
|
||||
SetState(ServiceState::Failed);
|
||||
|
||||
}
|
||||
|
||||
void AvahiService::client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *userdata)
|
||||
@@ -250,7 +326,7 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state)
|
||||
{
|
||||
case AVAHI_CLIENT_S_RUNNING:
|
||||
/* The server has startup successfully and registered its host
|
||||
* name on the network, so it's time to create our services */
|
||||
* name on the network, so it's time to create our services */
|
||||
create_group(c);
|
||||
break;
|
||||
case AVAHI_CLIENT_FAILURE:
|
||||
@@ -259,11 +335,11 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state)
|
||||
if (this->clientErrno == AVAHI_ERR_DISCONNECTED)
|
||||
{
|
||||
// tear the client down and restart it
|
||||
Lv2Log::info("Avahi connection lost. Reconnecting.");
|
||||
Lv2Log::info("DNS/SD connection lost. Reconnecting.");
|
||||
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_reset(group);
|
||||
avahi_entry_group_free(group);
|
||||
group = nullptr;
|
||||
}
|
||||
|
||||
@@ -278,28 +354,35 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state)
|
||||
if (!client)
|
||||
{
|
||||
Lv2Log::error(SS("Failed to create client: " << avahi_strerror(error)));
|
||||
terminated = true;
|
||||
avahi_threaded_poll_quit(threadedPoll);
|
||||
SetState(ServiceState::Failed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::error(SS("Client failure: " << avahi_strerror(avahi_client_errno(c))));
|
||||
terminated = true;
|
||||
Lv2Log::error(SS("DNS/SD client failure: " << avahi_strerror(avahi_client_errno(c))));
|
||||
avahi_threaded_poll_quit(threadedPoll);
|
||||
SetState(ServiceState::Failed);
|
||||
}
|
||||
break;
|
||||
case AVAHI_CLIENT_S_COLLISION:
|
||||
/* Let's drop our registered services. When the server is back
|
||||
* in AVAHI_SERVER_RUNNING state we will register them
|
||||
* again with the new host name. */
|
||||
* in AVAHI_SERVER_RUNNING state we will register them
|
||||
* again with the new host name. */
|
||||
case AVAHI_CLIENT_S_REGISTERING:
|
||||
/* The server records are now being established. This
|
||||
* might be caused by a host name change. We need to wait
|
||||
* for our own records to register until the host name is
|
||||
* properly esatblished. */
|
||||
* might be caused by a host name change. We need to wait
|
||||
* for our own records to register until the host name is
|
||||
* properly esatblished. */
|
||||
if (group)
|
||||
avahi_entry_group_reset(group);
|
||||
SetState(ServiceState::Settling);
|
||||
break;
|
||||
case AVAHI_CLIENT_CONNECTING:
|
||||
SetState(ServiceState::Settling);
|
||||
break;
|
||||
case AVAHI_CLIENT_CONNECTING:;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +393,8 @@ void AvahiService::Start()
|
||||
int ret = 1;
|
||||
struct timeval tv;
|
||||
this->clientErrno = 0;
|
||||
while (true)
|
||||
SetState(ServiceState::Initializing);
|
||||
for (int retry = 0; retry < 3; ++retry)
|
||||
{
|
||||
/* Allocate main loop object */
|
||||
if (!this->threadedPoll)
|
||||
@@ -339,30 +423,129 @@ void AvahiService::Start()
|
||||
return;
|
||||
fail:
|
||||
Stop();
|
||||
sleep(1);
|
||||
SetState(ServiceState::Failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void AvahiService::Stop()
|
||||
{
|
||||
if (stopped)
|
||||
return;
|
||||
this->stopped = true;
|
||||
|
||||
/* Cleanup things */
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_reset(group);
|
||||
avahi_entry_group_free(group);
|
||||
group = nullptr;
|
||||
}
|
||||
if (client)
|
||||
{
|
||||
avahi_client_free(client);
|
||||
client = nullptr;
|
||||
}
|
||||
Unannounce(true);
|
||||
sleep(1); // let traffic setting. avahi doens't seem to shut down cleany otherwise.
|
||||
|
||||
if (threadedPoll)
|
||||
{
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (group)
|
||||
{
|
||||
avahi_entry_group_free(group);
|
||||
group = nullptr;
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
avahi_threaded_poll_lock(threadedPoll);
|
||||
if (client)
|
||||
{
|
||||
avahi_client_free(client);
|
||||
client = nullptr;
|
||||
}
|
||||
avahi_threaded_poll_unlock(threadedPoll);
|
||||
|
||||
avahi_threaded_poll_stop(threadedPoll);
|
||||
avahi_threaded_poll_free(threadedPoll);
|
||||
SetState(ServiceState::Closed);
|
||||
threadedPoll = nullptr;
|
||||
}
|
||||
|
||||
/* Cleanup things */
|
||||
if (avahiNameString)
|
||||
{
|
||||
avahi_free(avahiNameString);
|
||||
avahiNameString = nullptr;
|
||||
}
|
||||
Lv2Log::info("DNS/SD service stopped.");
|
||||
}
|
||||
|
||||
void AvahiService::Wait()
|
||||
{
|
||||
using clock = std::chrono::steady_clock;
|
||||
auto start = clock::now();
|
||||
auto waitTime = start + std::chrono::duration_cast<clock::duration>(std::chrono::seconds(5));
|
||||
while (true)
|
||||
{
|
||||
bool done;
|
||||
switch (serviceState)
|
||||
{
|
||||
default:
|
||||
case ServiceState::Unitialized:
|
||||
throw std::runtime_error("Invalid state");
|
||||
|
||||
case ServiceState::Initializing:
|
||||
case ServiceState::Settling:
|
||||
case ServiceState::Requested:
|
||||
case ServiceState::Uncommited:
|
||||
case ServiceState::Registering:
|
||||
case ServiceState::Collision:
|
||||
done = false;
|
||||
break;
|
||||
case ServiceState::Reset:
|
||||
case ServiceState::Established:
|
||||
case ServiceState::Failed:
|
||||
case ServiceState::Closed:
|
||||
done = true;
|
||||
break;
|
||||
};
|
||||
if (done) break;
|
||||
if (clock::now() > waitTime)
|
||||
{
|
||||
Lv2Log::error("DNS/SD announcement timed out.");
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
}
|
||||
void AvahiService::WaitForUnannounce()
|
||||
{
|
||||
using clock = std::chrono::steady_clock;
|
||||
auto start = clock::now();
|
||||
auto waitTime = start + std::chrono::duration_cast<clock::duration>(std::chrono::seconds(5));
|
||||
|
||||
while (true)
|
||||
{
|
||||
bool done;
|
||||
switch (serviceState)
|
||||
{
|
||||
default:
|
||||
throw std::runtime_error("Invalid state");
|
||||
|
||||
case ServiceState::Initializing:
|
||||
case ServiceState::Settling:
|
||||
case ServiceState::Requested:
|
||||
case ServiceState::Registering:
|
||||
case ServiceState::Collision:
|
||||
done = false;
|
||||
break;
|
||||
case ServiceState::Uncommited:
|
||||
case ServiceState::Unitialized:
|
||||
case ServiceState::Reset:
|
||||
case ServiceState::Established:
|
||||
case ServiceState::Failed:
|
||||
case ServiceState::Closed:
|
||||
done = true;
|
||||
break;
|
||||
};
|
||||
if (done) break;
|
||||
if (clock::now() > waitTime)
|
||||
{
|
||||
Lv2Log::error("DNS/SD unannounce timed out.");
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user