Files
allwpilib/src/Storage.cpp
Peter Johnson 424bf51a7b Implement local notification.
The default behavior is to only notify remote changes, but for some
applications (e.g. GUI's) it's advantageous to know about local
changes as well.

This is (slightly) optimized in that local changes only result in
additional resources being consumed if (any) local listeners have been
created.
2015-09-23 00:56:08 -07:00

1301 lines
40 KiB
C++

/*----------------------------------------------------------------------------*/
/* Copyright (c) FIRST 2015. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "Storage.h"
#include <cctype>
#include <string>
#include <tuple>
#include "llvm/StringExtras.h"
#include "Base64.h"
#include "Log.h"
#include "NetworkConnection.h"
using namespace nt;
ATOMIC_STATIC_INIT(Storage)
Storage::Storage()
: Storage(Notifier::GetInstance(), RpcServer::GetInstance()) {}
Storage::Storage(Notifier& notifier, RpcServer& rpc_server)
: m_notifier(notifier), m_rpc_server(rpc_server) {
m_terminating = false;
}
Storage::~Storage() {
Logger::GetInstance().SetLogger(nullptr);
m_terminating = true;
m_rpc_results_cond.notify_all();
}
void Storage::SetOutgoing(QueueOutgoingFunc queue_outgoing, bool server) {
std::lock_guard<std::mutex> lock(m_mutex);
m_queue_outgoing = queue_outgoing;
m_server = server;
}
void Storage::ClearOutgoing() {
m_queue_outgoing = nullptr;
}
NT_Type Storage::GetEntryType(unsigned int id) const {
std::lock_guard<std::mutex> lock(m_mutex);
if (id >= m_idmap.size()) return NT_UNASSIGNED;
Entry* entry = m_idmap[id];
if (!entry || !entry->value) return NT_UNASSIGNED;
return entry->value->type();
}
void Storage::ProcessIncoming(std::shared_ptr<Message> msg,
NetworkConnection* conn,
std::weak_ptr<NetworkConnection> conn_weak) {
std::unique_lock<std::mutex> lock(m_mutex);
switch (msg->type()) {
case Message::kKeepAlive:
break; // ignore
case Message::kClientHello:
case Message::kProtoUnsup:
case Message::kServerHelloDone:
case Message::kServerHello:
case Message::kClientHelloDone:
// shouldn't get these, but ignore if we do
break;
case Message::kEntryAssign: {
unsigned int id = msg->id();
StringRef name = msg->str();
Entry* entry;
bool may_need_update = false;
if (m_server) {
// if we're a server, id=0xffff requests are requests for an id
// to be assigned, and we need to send the new assignment back to
// the sender as well as all other connections.
if (id == 0xffff) {
// see if it was already assigned; ignore if so.
if (m_entries.count(name) != 0) return;
// create it locally
id = m_idmap.size();
auto& new_entry = m_entries[name];
if (!new_entry) new_entry.reset(new Entry(name));
entry = new_entry.get();
entry->value = msg->value();
entry->flags = msg->flags();
entry->id = id;
m_idmap.push_back(entry);
// update persistent dirty flag if it's persistent
if (entry->IsPersistent()) m_persistent_dirty = true;
// notify
m_notifier.NotifyEntry(name, entry->value, true, false);
// send the assignment to everyone (including the originator)
if (m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
auto outmsg = Message::EntryAssign(
name, id, entry->seq_num.value(), msg->value(), msg->flags());
lock.unlock();
queue_outgoing(outmsg, nullptr, nullptr);
}
return;
}
if (id >= m_idmap.size() || !m_idmap[id]) {
// ignore arbitrary entry assignments
// this can happen due to e.g. assignment to deleted entry
lock.unlock();
DEBUG("server: received assignment to unknown entry");
return;
}
entry = m_idmap[id];
} else {
// clients simply accept new assignments
if (id == 0xffff) {
lock.unlock();
DEBUG("client: received entry assignment request?");
return;
}
if (id >= m_idmap.size()) m_idmap.resize(id+1);
entry = m_idmap[id];
if (!entry) {
// create local
auto& new_entry = m_entries[name];
if (!new_entry) {
// didn't exist at all (rather than just being a response to a
// id assignment request)
new_entry.reset(new Entry(name));
new_entry->value = msg->value();
new_entry->flags = msg->flags();
new_entry->id = id;
m_idmap[id] = new_entry.get();
// notify
m_notifier.NotifyEntry(name, new_entry->value, true, false);
return;
}
may_need_update = true; // we may need to send an update message
entry = new_entry.get();
entry->id = id;
m_idmap[id] = entry;
}
}
// common client and server handling
// already exists; ignore if sequence number not higher than local
SequenceNumber seq_num(msg->seq_num_uid());
if (seq_num < entry->seq_num) {
if (may_need_update) {
auto queue_outgoing = m_queue_outgoing;
auto outmsg = Message::EntryUpdate(entry->id, entry->seq_num.value(),
entry->value);
lock.unlock();
queue_outgoing(outmsg, nullptr, nullptr);
}
return;
}
// sanity check: name should match id
if (msg->str() != entry->name) {
lock.unlock();
DEBUG("entry assignment for same id with different name?");
return;
}
// don't update flags from a <3.0 remote (not part of message)
if (conn->proto_rev() >= 0x0300) {
// update persistent dirty flag if persistent flag changed
if ((entry->flags & NT_PERSISTENT) != (msg->flags() & NT_PERSISTENT))
m_persistent_dirty = true;
entry->flags = msg->flags();
}
// update persistent dirty flag if the value changed and it's persistent
if (entry->IsPersistent() && *entry->value != *msg->value())
m_persistent_dirty = true;
// update local
entry->value = msg->value();
entry->seq_num = seq_num;
// notify
m_notifier.NotifyEntry(name, entry->value, false, false);
// broadcast to all other connections (note for client there won't
// be any other connections, so don't bother)
if (m_server && m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
auto outmsg =
Message::EntryAssign(entry->name, id, msg->seq_num_uid(),
msg->value(), entry->flags);
lock.unlock();
queue_outgoing(outmsg, nullptr, conn);
}
break;
}
case Message::kEntryUpdate: {
unsigned int id = msg->id();
if (id >= m_idmap.size() || !m_idmap[id]) {
// ignore arbitrary entry updates;
// this can happen due to deleted entries
lock.unlock();
DEBUG("received update to unknown entry");
return;
}
Entry* entry = m_idmap[id];
// ignore if sequence number not higher than local
SequenceNumber seq_num(msg->seq_num_uid());
if (seq_num <= entry->seq_num) return;
// update local
entry->value = msg->value();
entry->seq_num = seq_num;
// update persistent dirty flag if it's a persistent value
if (entry->IsPersistent()) m_persistent_dirty = true;
// notify
m_notifier.NotifyEntry(entry->name, entry->value, false, false);
// broadcast to all other connections (note for client there won't
// be any other connections, so don't bother)
if (m_server && m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(msg, nullptr, conn);
}
break;
}
case Message::kFlagsUpdate: {
unsigned int id = msg->id();
if (id >= m_idmap.size() || !m_idmap[id]) {
// ignore arbitrary entry updates;
// this can happen due to deleted entries
lock.unlock();
DEBUG("received flags update to unknown entry");
return;
}
Entry* entry = m_idmap[id];
// update persistent dirty flag if persistent flag changed
if ((entry->flags & NT_PERSISTENT) != (msg->flags() & NT_PERSISTENT))
m_persistent_dirty = true;
// update local
entry->flags = msg->flags();
// broadcast to all other connections (note for client there won't
// be any other connections, so don't bother)
if (m_server && m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(msg, nullptr, conn);
}
break;
}
case Message::kEntryDelete: {
unsigned int id = msg->id();
if (id >= m_idmap.size() || !m_idmap[id]) {
// ignore arbitrary entry updates;
// this can happen due to deleted entries
lock.unlock();
DEBUG("received delete to unknown entry");
return;
}
Entry* entry = m_idmap[id];
// update persistent dirty flag if it's a persistent value
if (entry->IsPersistent()) m_persistent_dirty = true;
// update local
m_entries.erase(entry->name); // erase from map
m_idmap[id] = nullptr; // delete it from idmap too
// broadcast to all other connections (note for client there won't
// be any other connections, so don't bother)
if (m_server && m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(msg, nullptr, conn);
}
break;
}
case Message::kClearEntries: {
// update local
m_entries.clear();
m_idmap.resize(0);
// set persistent dirty flag
m_persistent_dirty = true;
// broadcast to all other connections (note for client there won't
// be any other connections, so don't bother)
if (m_server && m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(msg, nullptr, conn);
}
break;
}
case Message::kExecuteRpc: {
if (!m_server) return; // only process on server
unsigned int id = msg->id();
if (id >= m_idmap.size() || !m_idmap[id]) {
// ignore call to non-existent RPC
// this can happen due to deleted entries
lock.unlock();
DEBUG("received RPC call to unknown entry");
return;
}
Entry* entry = m_idmap[id];
if (!entry->value->IsRpc()) {
lock.unlock();
DEBUG("received RPC call to non-RPC entry");
return;
}
m_rpc_server.ProcessRpc(entry->name, msg, entry->rpc_callback,
conn->uid(), [=](std::shared_ptr<Message> msg) {
auto c = conn_weak.lock();
if (c) c->QueueOutgoing(msg);
});
break;
}
case Message::kRpcResponse: {
if (m_server) return; // only process on client
m_rpc_results.insert(std::make_pair(
std::make_pair(msg->id(), msg->seq_num_uid()), msg->str()));
m_rpc_results_cond.notify_all();
break;
}
default:
break;
}
}
void Storage::GetInitialAssignments(
NetworkConnection& conn, std::vector<std::shared_ptr<Message>>* msgs) {
std::lock_guard<std::mutex> lock(m_mutex);
conn.set_state(NetworkConnection::kSynchronized);
for (auto& i : m_entries) {
Entry* entry = i.getValue().get();
msgs->emplace_back(Message::EntryAssign(i.getKey(), entry->id,
entry->seq_num.value(),
entry->value, entry->flags));
}
}
void Storage::ApplyInitialAssignments(
NetworkConnection& conn, llvm::ArrayRef<std::shared_ptr<Message>> msgs,
bool new_server, std::vector<std::shared_ptr<Message>>* out_msgs) {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_server) return; // should not do this on server
conn.set_state(NetworkConnection::kSynchronized);
std::vector<std::shared_ptr<Message>> update_msgs;
// clear existing id's
for (auto& i : m_entries) i.getValue()->id = 0xffff;
// clear existing idmap
m_idmap.resize(0);
// apply assignments
for (auto& msg : msgs) {
if (!msg->Is(Message::kEntryAssign)) {
DEBUG("client: received non-entry assignment request?");
continue;
}
unsigned int id = msg->id();
if (id == 0xffff) {
DEBUG("client: received entry assignment request?");
continue;
}
SequenceNumber seq_num(msg->seq_num_uid());
StringRef name = msg->str();
auto& entry = m_entries[name];
if (!entry) {
// doesn't currently exist
entry.reset(new Entry(name));
entry->value = msg->value();
entry->flags = msg->flags();
entry->seq_num = seq_num;
// notify
m_notifier.NotifyEntry(name, entry->value, true, false);
} else {
// if reconnect and sequence number not higher than local, then we
// don't update the local value and instead send it back to the server
// as an update message
if (!new_server && seq_num <= entry->seq_num) {
update_msgs.emplace_back(Message::EntryUpdate(
entry->id, entry->seq_num.value(), entry->value));
} else {
entry->value = msg->value();
entry->seq_num = seq_num;
// don't update flags from a <3.0 remote (not part of message)
if (conn.proto_rev() >= 0x0300) entry->flags = msg->flags();
// notify
m_notifier.NotifyEntry(name, entry->value, false, false);
}
}
// set id and save to idmap
entry->id = id;
if (id >= m_idmap.size()) m_idmap.resize(id+1);
m_idmap[id] = entry.get();
}
// generate assign messages for unassigned local entries
for (auto& i : m_entries) {
Entry* entry = i.getValue().get();
if (entry->id != 0xffff) continue;
out_msgs->emplace_back(Message::EntryAssign(entry->name, entry->id,
entry->seq_num.value(),
entry->value, entry->flags));
}
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
for (auto& msg : update_msgs) queue_outgoing(msg, nullptr, nullptr);
}
std::shared_ptr<Value> Storage::GetEntryValue(StringRef name) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto i = m_entries.find(name);
return i == m_entries.end() ? nullptr : i->getValue()->value;
}
bool Storage::SetEntryValue(StringRef name, std::shared_ptr<Value> value) {
if (name.empty()) return true;
if (!value) return true;
std::unique_lock<std::mutex> lock(m_mutex);
auto& new_entry = m_entries[name];
if (!new_entry) new_entry.reset(new Entry(name));
Entry* entry = new_entry.get();
auto old_value = entry->value;
if (old_value && old_value->type() != value->type())
return false; // error on type mismatch
entry->value = value;
// if we're the server, assign an id if it doesn't have one
if (m_server && entry->id == 0xffff) {
unsigned int id = m_idmap.size();
entry->id = id;
m_idmap.push_back(entry);
}
// update persistent dirty flag if value changed and it's persistent
if (entry->IsPersistent() && *old_value != *value) m_persistent_dirty = true;
// generate message
if (!m_queue_outgoing) return true;
auto queue_outgoing = m_queue_outgoing;
if (!old_value) {
auto msg = Message::EntryAssign(name, entry->id, entry->seq_num.value(),
value, entry->flags);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
m_notifier.NotifyEntry(name, value, true, true);
} else if (*old_value != *value) {
++entry->seq_num;
// don't send an update if we don't have an assigned id yet
if (entry->id != 0xffff) {
auto msg =
Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
}
m_notifier.NotifyEntry(name, value, false, true);
}
return true;
}
void Storage::SetEntryTypeValue(StringRef name, std::shared_ptr<Value> value) {
if (name.empty()) return;
if (!value) return;
std::unique_lock<std::mutex> lock(m_mutex);
auto& new_entry = m_entries[name];
if (!new_entry) new_entry.reset(new Entry(name));
Entry* entry = new_entry.get();
auto old_value = entry->value;
entry->value = value;
if (old_value && *old_value == *value) return;
// if we're the server, assign an id if it doesn't have one
if (m_server && entry->id == 0xffff) {
unsigned int id = m_idmap.size();
entry->id = id;
m_idmap.push_back(entry);
}
// update persistent dirty flag if it's a persistent value
if (entry->IsPersistent()) m_persistent_dirty = true;
// generate message
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
if (!old_value || old_value->type() != value->type()) {
++entry->seq_num;
auto msg = Message::EntryAssign(name, entry->id, entry->seq_num.value(),
value, entry->flags);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
m_notifier.NotifyEntry(name, value, true, true);
} else {
++entry->seq_num;
// don't send an update if we don't have an assigned id yet
if (entry->id != 0xffff) {
auto msg =
Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
}
m_notifier.NotifyEntry(name, value, false, true);
}
}
void Storage::SetEntryFlags(StringRef name, unsigned int flags) {
if (name.empty()) return;
std::unique_lock<std::mutex> lock(m_mutex);
auto i = m_entries.find(name);
if (i == m_entries.end()) return;
Entry* entry = i->getValue().get();
if (entry->flags == flags) return;
// update persistent dirty flag if persistent flag changed
if ((entry->flags & NT_PERSISTENT) != (flags & NT_PERSISTENT))
m_persistent_dirty = true;
entry->flags = flags;
// generate message
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
unsigned int id = entry->id;
// don't send an update if we don't have an assigned id yet
if (id != 0xffff) {
lock.unlock();
queue_outgoing(Message::FlagsUpdate(id, flags), nullptr, nullptr);
}
}
unsigned int Storage::GetEntryFlags(StringRef name) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto i = m_entries.find(name);
return i == m_entries.end() ? 0 : i->getValue()->flags;
}
void Storage::DeleteEntry(StringRef name) {
std::unique_lock<std::mutex> lock(m_mutex);
auto i = m_entries.find(name);
if (i == m_entries.end()) return;
Entry* entry = i->getValue().get();
unsigned int id = entry->id;
bool had_value = entry->value != nullptr;
// update persistent dirty flag if it's a persistent value
if (entry->IsPersistent()) m_persistent_dirty = true;
m_entries.erase(i); // erase from map
if (id < m_idmap.size()) m_idmap[id] = nullptr;
if (!had_value) return;
// if it had a value, generate message
// don't send an update if we don't have an assigned id yet
if (id != 0xffff) {
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(Message::EntryDelete(id), nullptr, nullptr);
}
}
void Storage::DeleteAllEntries() {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_entries.empty()) return;
m_entries.clear();
m_idmap.resize(0);
// set persistent dirty flag
m_persistent_dirty = true;
// generate message
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(Message::ClearEntries(), nullptr, nullptr);
}
std::vector<EntryInfo> Storage::GetEntryInfo(StringRef prefix,
unsigned int types) {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<EntryInfo> infos;
for (auto& i : m_entries) {
if (!i.getKey().startswith(prefix)) continue;
Entry* entry = i.getValue().get();
auto value = entry->value;
if (!value) continue;
if (types != 0 && (types & value->type()) == 0) continue;
EntryInfo info;
info.name = i.getKey();
info.type = value->type();
info.flags = entry->flags;
info.last_change = value->last_change();
infos.push_back(std::move(info));
}
return infos;
}
void Storage::NotifyEntries(StringRef prefix,
EntryListenerCallback only) const {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& i : m_entries) {
if (!i.getKey().startswith(prefix)) continue;
m_notifier.NotifyEntry(i.getKey(), i.getValue()->value, false, false, only);
}
}
/* Escapes and writes a string, including start and end double quotes */
static void WriteString(std::ostream& os, llvm::StringRef str) {
os << '"';
for (auto c : str) {
switch (c) {
case '\\':
os << "\\\\";
break;
case '\t':
os << "\\t";
break;
case '\n':
os << "\\n";
break;
case '"':
os << "\\\"";
break;
default:
if (std::isprint(c)) {
os << c;
break;
}
// Write out the escaped representation.
os << "\\x";
os << llvm::hexdigit((c >> 4) & 0xF);
os << llvm::hexdigit((c >> 0) & 0xF);
}
}
os << '"';
}
bool Storage::GetPersistentEntries(
bool periodic,
std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
const {
// copy values out of storage as quickly as possible so lock isn't held
{
std::lock_guard<std::mutex> lock(m_mutex);
// for periodic, don't re-save unless something has changed
if (periodic && !m_persistent_dirty) return false;
m_persistent_dirty = false;
entries->reserve(m_entries.size());
for (auto& i : m_entries) {
Entry* entry = i.getValue().get();
// only write persistent-flagged values
if (!entry->IsPersistent()) continue;
entries->emplace_back(i.getKey(), entry->value);
}
}
// sort in name order
std::sort(entries->begin(), entries->end(),
[](const std::pair<std::string, std::shared_ptr<Value>>& a,
const std::pair<std::string, std::shared_ptr<Value>>& b) {
return a.first < b.first;
});
return true;
}
static void SavePersistentImpl(
std::ostream& os,
llvm::ArrayRef<std::pair<std::string, std::shared_ptr<Value>>> entries) {
std::string base64_encoded;
// header
os << "[NetworkTables Storage 3.0]\n";
for (auto& i : entries) {
// type
auto v = i.second;
if (!v) continue;
switch (v->type()) {
case NT_BOOLEAN:
os << "boolean ";
break;
case NT_DOUBLE:
os << "double ";
break;
case NT_STRING:
os << "string ";
break;
case NT_RAW:
os << "raw ";
break;
case NT_BOOLEAN_ARRAY:
os << "array boolean ";
break;
case NT_DOUBLE_ARRAY:
os << "array double ";
break;
case NT_STRING_ARRAY:
os << "array string ";
break;
default:
continue;
}
// name
WriteString(os, i.first);
// =
os << '=';
// value
switch (v->type()) {
case NT_BOOLEAN:
os << (v->GetBoolean() ? "true" : "false");
break;
case NT_DOUBLE:
os << v->GetDouble();
break;
case NT_STRING:
WriteString(os, v->GetString());
break;
case NT_RAW:
Base64Encode(v->GetRaw(), &base64_encoded);
os << base64_encoded;
break;
case NT_BOOLEAN_ARRAY: {
bool first = true;
for (auto elem : v->GetBooleanArray()) {
if (!first) os << ',';
first = false;
os << (elem ? "true" : "false");
}
break;
}
case NT_DOUBLE_ARRAY: {
bool first = true;
for (auto elem : v->GetDoubleArray()) {
if (!first) os << ',';
first = false;
os << elem;
}
break;
}
case NT_STRING_ARRAY: {
bool first = true;
for (auto& elem : v->GetStringArray()) {
if (!first) os << ',';
first = false;
WriteString(os, elem);
}
break;
}
default:
break;
}
// eol
os << '\n';
}
}
void Storage::SavePersistent(std::ostream& os, bool periodic) const {
std::vector<std::pair<std::string, std::shared_ptr<Value>>> entries;
if (!GetPersistentEntries(periodic, &entries)) return;
SavePersistentImpl(os, entries);
}
const char* Storage::SavePersistent(StringRef filename, bool periodic) const {
std::string fn = filename;
std::string tmp = filename;
tmp += ".tmp";
std::string bak = filename;
bak += ".bak";
// Get entries before creating file
std::vector<std::pair<std::string, std::shared_ptr<Value>>> entries;
if (!GetPersistentEntries(periodic, &entries)) return nullptr;
const char* err = nullptr;
// start by writing to temporary file
std::ofstream os(tmp);
if (!os) {
err = "could not open file";
goto done;
}
DEBUG("saving persistent file '" << filename << "'");
SavePersistentImpl(os, entries);
os.flush();
if (!os) {
os.close();
std::remove(tmp.c_str());
err = "error saving file";
goto done;
}
os.close();
// Safely move to real file. We ignore any failures related to the backup.
std::remove(bak.c_str());
std::rename(fn.c_str(), bak.c_str());
if (std::rename(tmp.c_str(), fn.c_str()) != 0) {
std::rename(bak.c_str(), fn.c_str()); // attempt to restore backup
err = "could not rename temp file to real file";
goto done;
}
done:
// try again if there was an error
if (err && periodic) m_persistent_dirty = true;
return err;
}
/* Extracts an escaped string token. Does not unescape the string.
* If a string cannot be matched, an empty string is returned.
* If the string is unterminated, an empty tail string is returned.
* The returned token includes the starting and trailing quotes (unless the
* string is unterminated).
* Returns a pair containing the extracted token (if any) and the remaining
* tail string.
*/
static std::pair<llvm::StringRef, llvm::StringRef> ReadStringToken(
llvm::StringRef source) {
// Match opening quote
if (source.empty() || source.front() != '"')
return std::make_pair(llvm::StringRef(), source);
// Scan for ending double quote, checking for escaped as we go.
std::size_t size = source.size();
std::size_t pos;
for (pos = 1; pos < size; ++pos) {
if (source[pos] == '"' && source[pos - 1] != '\\') {
++pos; // we want to include the trailing quote in the result
break;
}
}
return std::make_pair(source.slice(0, pos), source.substr(pos));
}
static int fromxdigit(char ch) {
if (ch >= 'a' && ch <= 'f')
return (ch - 'a' + 10);
else if (ch >= 'A' && ch <= 'F')
return (ch - 'A' + 10);
else
return ch - '0';
}
static void UnescapeString(llvm::StringRef source, std::string* dest) {
assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
dest->clear();
dest->reserve(source.size() - 2);
for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
if (*s != '\\') {
dest->push_back(*s);
continue;
}
switch (*++s) {
case '\\':
case '"':
dest->push_back(s[-1]);
break;
case 't':
dest->push_back('\t');
break;
case 'n':
dest->push_back('\n');
break;
case 'x': {
if (!isxdigit(*(s+1))) {
dest->push_back('x'); // treat it like a unknown escape
break;
}
int ch = fromxdigit(*++s);
if (isxdigit(*(s+1))) {
ch <<= 4;
ch |= fromxdigit(*++s);
}
dest->push_back(static_cast<char>(ch));
break;
}
default:
dest->push_back(s[-1]);
break;
}
}
}
bool Storage::LoadPersistent(
std::istream& is,
std::function<void(std::size_t line, const char* msg)> warn) {
std::string line_str;
std::size_t line_num = 1;
// entries to add
std::vector<std::pair<std::string, std::shared_ptr<Value>>> entries;
// declare these outside the loop to reduce reallocs
std::string name, str;
std::vector<int> boolean_array;
std::vector<double> double_array;
std::vector<std::string> string_array;
// ignore blank lines and lines that start with ; or # (comments)
while (std::getline(is, line_str)) {
llvm::StringRef line = llvm::StringRef(line_str).trim();
if (!line.empty() && line.front() != ';' && line.front() != '#')
break;
}
// header
if (line_str != "[NetworkTables Storage 3.0]") {
if (warn) warn(line_num, "header line mismatch, ignoring rest of file");
return false;
}
while (std::getline(is, line_str)) {
llvm::StringRef line = llvm::StringRef(line_str).trim();
++line_num;
// ignore blank lines and lines that start with ; or # (comments)
if (line.empty() || line.front() == ';' || line.front() == '#')
continue;
// type
llvm::StringRef type_tok;
std::tie(type_tok, line) = line.split(' ');
NT_Type type = NT_UNASSIGNED;
if (type_tok == "boolean") type = NT_BOOLEAN;
else if (type_tok == "double") type = NT_DOUBLE;
else if (type_tok == "string") type = NT_STRING;
else if (type_tok == "raw") type = NT_RAW;
else if (type_tok == "array") {
llvm::StringRef array_tok;
std::tie(array_tok, line) = line.split(' ');
if (array_tok == "boolean") type = NT_BOOLEAN_ARRAY;
else if (array_tok == "double") type = NT_DOUBLE_ARRAY;
else if (array_tok == "string") type = NT_STRING_ARRAY;
}
if (type == NT_UNASSIGNED) {
if (warn) warn(line_num, "unrecognized type");
continue;
}
// name
llvm::StringRef name_tok;
std::tie(name_tok, line) = ReadStringToken(line);
if (name_tok.empty()) {
if (warn) warn(line_num, "missing name");
continue;
}
if (name_tok.back() != '"') {
if (warn) warn(line_num, "unterminated name string");
continue;
}
UnescapeString(name_tok, &name);
// =
line = line.ltrim(" \t");
if (line.empty() || line.front() != '=') {
if (warn) warn(line_num, "expected = after name");
continue;
}
line = line.drop_front().ltrim(" \t");
// value
std::shared_ptr<Value> value;
switch (type) {
case NT_BOOLEAN:
// only true or false is accepted
if (line == "true")
value = Value::MakeBoolean(true);
else if (line == "false")
value = Value::MakeBoolean(false);
else {
if (warn)
warn(line_num, "unrecognized boolean value, not 'true' or 'false'");
goto next_line;
}
break;
case NT_DOUBLE: {
// need to convert to null-terminated string for strtod()
str.clear();
str += line;
char* end;
double v = std::strtod(str.c_str(), &end);
if (*end != '\0') {
if (warn) warn(line_num, "invalid double value");
goto next_line;
}
value = Value::MakeDouble(v);
break;
}
case NT_STRING: {
llvm::StringRef str_tok;
std::tie(str_tok, line) = ReadStringToken(line);
if (str_tok.empty()) {
if (warn) warn(line_num, "missing string value");
goto next_line;
}
if (str_tok.back() != '"') {
if (warn) warn(line_num, "unterminated string value");
goto next_line;
}
UnescapeString(str_tok, &str);
value = Value::MakeString(std::move(str));
break;
}
case NT_RAW:
Base64Decode(line, &str);
value = Value::MakeRaw(std::move(str));
break;
case NT_BOOLEAN_ARRAY: {
llvm::StringRef elem_tok;
boolean_array.clear();
while (!line.empty()) {
std::tie(elem_tok, line) = line.split(',');
elem_tok = elem_tok.trim(" \t");
if (elem_tok == "true")
boolean_array.push_back(1);
else if (elem_tok == "false")
boolean_array.push_back(0);
else {
if (warn)
warn(line_num,
"unrecognized boolean value, not 'true' or 'false'");
goto next_line;
}
}
value = Value::MakeBooleanArray(std::move(boolean_array));
break;
}
case NT_DOUBLE_ARRAY: {
llvm::StringRef elem_tok;
double_array.clear();
while (!line.empty()) {
std::tie(elem_tok, line) = line.split(',');
elem_tok = elem_tok.trim(" \t");
// need to convert to null-terminated string for strtod()
str.clear();
str += elem_tok;
char* end;
double v = std::strtod(str.c_str(), &end);
if (*end != '\0') {
if (warn) warn(line_num, "invalid double value");
goto next_line;
}
double_array.push_back(v);
}
value = Value::MakeDoubleArray(std::move(double_array));
break;
}
case NT_STRING_ARRAY: {
llvm::StringRef elem_tok;
string_array.clear();
while (!line.empty()) {
std::tie(elem_tok, line) = ReadStringToken(line);
if (elem_tok.empty()) {
if (warn) warn(line_num, "missing string value");
goto next_line;
}
if (elem_tok.back() != '"') {
if (warn) warn(line_num, "unterminated string value");
goto next_line;
}
UnescapeString(elem_tok, &str);
string_array.push_back(std::move(str));
line = line.ltrim(" \t");
if (line.empty()) break;
if (line.front() != ',') {
if (warn) warn(line_num, "expected comma between strings");
goto next_line;
}
line = line.drop_front().ltrim(" \t");
}
value = Value::MakeStringArray(std::move(string_array));
break;
}
default:
break;
}
if (!name.empty() && value)
entries.push_back(std::make_pair(std::move(name), std::move(value)));
next_line:
;
}
// copy values into storage as quickly as possible so lock isn't held
{
std::vector<std::shared_ptr<Message>> msgs;
std::unique_lock<std::mutex> lock(m_mutex);
for (auto& i : entries) {
auto& new_entry = m_entries[i.first];
if (!new_entry) new_entry.reset(new Entry(i.first));
Entry* entry = new_entry.get();
auto old_value = entry->value;
entry->value = i.second;
bool was_persist = entry->IsPersistent();
if (!was_persist) entry->flags |= NT_PERSISTENT;
// if we're the server, assign an id if it doesn't have one
if (m_server && entry->id == 0xffff) {
unsigned int id = m_idmap.size();
entry->id = id;
m_idmap.push_back(entry);
}
if (!m_queue_outgoing) continue; // shortcut
++entry->seq_num;
// put on update queue
if (!old_value || old_value->type() != i.second->type())
msgs.emplace_back(Message::EntryAssign(i.first, entry->id,
entry->seq_num.value(),
i.second, entry->flags));
else if (entry->id != 0xffff) {
// don't send an update if we don't have an assigned id yet
if (*old_value != *i.second)
msgs.emplace_back(Message::EntryUpdate(
entry->id, entry->seq_num.value(), i.second));
if (!was_persist)
msgs.emplace_back(Message::FlagsUpdate(entry->id, entry->flags));
}
}
if (m_queue_outgoing) {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
for (auto& msg : msgs) queue_outgoing(std::move(msg), nullptr, nullptr);
}
}
return true;
}
const char* Storage::LoadPersistent(
StringRef filename,
std::function<void(std::size_t line, const char* msg)> warn) {
std::ifstream is(filename);
if (!is) return "could not open file";
if (!LoadPersistent(is, warn)) return "error reading file";
return nullptr;
}
void Storage::CreateRpc(StringRef name, StringRef def, RpcCallback callback) {
if (name.empty() || def.empty() || !callback) return;
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_server) return; // only server can create RPCs
auto& new_entry = m_entries[name];
if (!new_entry) new_entry.reset(new Entry(name));
Entry* entry = new_entry.get();
auto old_value = entry->value;
auto value = Value::MakeRpc(def);
entry->value = value;
// set up the new callback
entry->rpc_callback = callback;
// start the RPC server
if (!m_rpc_server.active()) m_rpc_server.Start();
if (old_value && *old_value == *value) return;
// assign an id if it doesn't have one
if (entry->id == 0xffff) {
unsigned int id = m_idmap.size();
entry->id = id;
m_idmap.push_back(entry);
}
// generate message
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
if (!old_value || old_value->type() != value->type()) {
++entry->seq_num;
auto msg = Message::EntryAssign(name, entry->id, entry->seq_num.value(),
value, entry->flags);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
} else {
++entry->seq_num;
auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
}
}
void Storage::CreatePolledRpc(StringRef name, StringRef def) {
if (name.empty() || def.empty()) return;
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_server) return; // only server can create RPCs
auto& new_entry = m_entries[name];
if (!new_entry) new_entry.reset(new Entry(name));
Entry* entry = new_entry.get();
auto old_value = entry->value;
auto value = Value::MakeRpc(def);
entry->value = value;
// a nullptr callback indicates a polled RPC
entry->rpc_callback = nullptr;
if (old_value && *old_value == *value) return;
// assign an id if it doesn't have one
if (entry->id == 0xffff) {
unsigned int id = m_idmap.size();
entry->id = id;
m_idmap.push_back(entry);
}
// generate message
if (!m_queue_outgoing) return;
auto queue_outgoing = m_queue_outgoing;
if (!old_value || old_value->type() != value->type()) {
++entry->seq_num;
auto msg = Message::EntryAssign(name, entry->id, entry->seq_num.value(),
value, entry->flags);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
} else {
++entry->seq_num;
auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
}
}
unsigned int Storage::CallRpc(StringRef name, StringRef params) {
std::unique_lock<std::mutex> lock(m_mutex);
auto i = m_entries.find(name);
if (i == m_entries.end()) return 0;
auto& entry = i->getValue();
if (!entry->value->IsRpc()) return 0;
++entry->rpc_call_uid;
if (entry->rpc_call_uid > 0xffff) entry->rpc_call_uid = 0;
unsigned int combined_uid = (entry->id << 16) | entry->rpc_call_uid;
auto msg = Message::ExecuteRpc(entry->id, entry->rpc_call_uid, params);
if (m_server) {
// RPCs are unlikely to be used locally on the server, but handle it
// gracefully anyway.
auto rpc_callback = entry->rpc_callback;
lock.unlock();
m_rpc_server.ProcessRpc(
name, msg, rpc_callback, 0xffffU, [this](std::shared_ptr<Message> msg) {
std::lock_guard<std::mutex> lock(m_mutex);
m_rpc_results.insert(std::make_pair(
std::make_pair(msg->id(), msg->seq_num_uid()), msg->str()));
m_rpc_results_cond.notify_all();
});
} else {
auto queue_outgoing = m_queue_outgoing;
lock.unlock();
queue_outgoing(msg, nullptr, nullptr);
}
return combined_uid;
}
bool Storage::GetRpcResult(bool blocking, unsigned int call_uid,
std::string* result) {
std::unique_lock<std::mutex> lock(m_mutex);
for (;;) {
auto i =
m_rpc_results.find(std::make_pair(call_uid >> 16, call_uid & 0xffff));
if (i == m_rpc_results.end()) {
if (!blocking || m_terminating) return false;
m_rpc_results_cond.wait(lock);
continue;
}
result->swap(i->getSecond());
m_rpc_results.erase(i);
return true;
}
}