Skip to content

Commit

Permalink
Issue a reset request in response to config file changes.
Browse files Browse the repository at this point in the history
Fixes #57.
  • Loading branch information
dechamps committed Sep 4, 2020
1 parent 59f0b2d commit 7f411c5
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 33 deletions.
9 changes: 5 additions & 4 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ example: `C:\Users\Your Name\FlexASIO.toml`.
If the file is missing, this is equivalent to supplying an empty file,
and as a result FlexASIO will use default values for everything.

Configuration changes will only take effect after FlexASIO is reinitialized.
Depending on the ASIO host application, this might require the application to be
restarted.

The configuration file is a text file that can be edited using any text editor,
such as Notepad. The file follows the [TOML][] syntax, which is very similar to
the syntax used for [INI files][]. Every feature described in the [official TOML documentation] should be supported.
Expand All @@ -28,6 +24,11 @@ value (which includes using the wrong type or missing quotes), FlexASIO
will *fail to initialize*. The [FlexASIO log][logging] will contain details
about what went wrong.

While running, FlexASIO watches for changes to the configuration file. If a
change is detected, FlexASIO will automatically issue a reset request to the
ASIO application. What happens next is up to the application; ideally, it should
reload FlexASIO and pick up the new configuration.

## Example configuration file

```toml
Expand Down
169 changes: 145 additions & 24 deletions src/flexasio/FlexASIO/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

#include "config.h"

#include <filesystem>

#include <toml/toml.h>

#include "log.h"
Expand All @@ -13,16 +11,9 @@ namespace flexasio {

namespace {

toml::Value LoadConfigToml() {
std::filesystem::path path;
try {
path = GetUserDirectory();
path.append("FlexASIO.toml");
}
catch (...) {
std::throw_with_nested(std::runtime_error("Unable to get user profile directory"));
}
constexpr auto configFileName = L"FlexASIO.toml";

toml::Value LoadConfigToml(const std::filesystem::path& path) {
Log() << "Attempting to load configuration file: " << path;

std::ifstream stream;
Expand Down Expand Up @@ -109,25 +100,155 @@ namespace flexasio {
ProcessTypedOption<toml::Table>(table, "output", [&](const toml::Table& table) { SetStream(table, config.output); });
}

}

Config LoadConfig() {
toml::Value tomlValue;
try {
tomlValue = LoadConfigToml();
Config LoadConfig(const std::filesystem::path& path) {
toml::Value tomlValue;
try {
tomlValue = LoadConfigToml(path);
}
catch (...) {
std::throw_with_nested(std::runtime_error("Unable to load configuration file"));
}

try {
Config config;
SetConfig(tomlValue.as<toml::Table>(), config);
return config;
}
catch (...) {
std::throw_with_nested(std::runtime_error("Invalid configuration"));
}
}
catch (...) {
std::throw_with_nested(std::runtime_error("Unable to load configuration file"));

}

void ConfigLoader::HandleCloser::operator()(HANDLE handle) {
if (::CloseHandle(handle) == 0)
throw std::system_error(::GetLastError(), std::system_category(), "unable to close handle");
}

ConfigLoader::Watcher::Watcher(std::function<void()> onConfigFileEvent, const std::filesystem::path& configDirectory) : onConfigFileEvent(std::move(onConfigFileEvent)), stopEvent([&] {
const auto handle = CreateEventA(NULL, TRUE, FALSE, NULL);
if (handle == NULL)
throw std::system_error(::GetLastError(), std::system_category(), "Unable to create stop event");
return UniqueHandle(handle);
}()), directory([&] {
Log() << "Opening config directory for watching";
const auto handle = ::CreateFileW(
configDirectory.wstring().c_str(),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
/*lpSecurityAttributes=*/NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
/*hTemplateFile=*/NULL);
if (handle == INVALID_HANDLE_VALUE)
throw std::system_error(::GetLastError(), std::system_category(), "Unable to open config directory for watching");
return UniqueHandle(handle);
}()) {
Log() << "Starting configuration file watcher";
StartWatching();
thread = std::thread([this] { RunThread(); });
}

ConfigLoader::Watcher::~Watcher() noexcept(false) {
if (!SetEvent(stopEvent.get()))
throw std::system_error(::GetLastError(), std::system_category(), "Unable to set stop event");
thread.join();
}

void ConfigLoader::Watcher::RunThread() {
// TODO: handle exceptions

Log() << "Config watcher thread running";

for (;;) {
std::array handles = { stopEvent.get(), overlapped.overlapped.hEvent };
const auto waitResult = ::WaitForMultipleObjects(DWORD(handles.size()), handles.data(), /*bWaitAll=*/FALSE, INFINITE);
if (waitResult == WAIT_OBJECT_0) break;
else if (waitResult == WAIT_OBJECT_0 + 1) OnEvent();
else throw std::system_error(::GetLastError(), std::system_category(), "Unable to wait for events");
}

try {
Config config;
SetConfig(tomlValue.as<toml::Table>(), config);
return config;
Log() << "Config watcher thread stopping";
}

void ConfigLoader::Watcher::OnEvent() {
// Note: we need to be careful about logging here - since the logfile is in the same directory as the config file,
// we could end up with directory change events entering an infinite feedback loop.

DWORD size;
if (!GetOverlappedResult(directory.get(), &overlapped.overlapped, &size, /*bWait=*/FALSE))
throw std::system_error(::GetLastError(), std::system_category(), "GetOverlappedResult() failed");
if (size <= 0) {
Log() << "Config directory event buffer overflow";
// We don't know if something happened to the logfile, so assume it did.
onConfigFileEvent();
}
catch (...) {
std::throw_with_nested(std::runtime_error("Invalid configuration"));
else {
const char* fileNotifyInformationPtr = fileNotifyInformationBuffer;
for (;;) {
constexpr auto fileNotifyInformationHeaderSize = offsetof(FILE_NOTIFY_INFORMATION, FileName);
FILE_NOTIFY_INFORMATION fileNotifyInformationHeader;
memcpy(&fileNotifyInformationHeader, fileNotifyInformationPtr, fileNotifyInformationHeaderSize);

std::wstring fileName(fileNotifyInformationHeader.FileNameLength / sizeof(wchar_t), 0);
memcpy(fileName.data(), fileNotifyInformationPtr + fileNotifyInformationHeaderSize, fileNotifyInformationHeader.FileNameLength);
if (fileName == configFileName) {
// Here we can safely log.
Log() << "Configuration file directory change received: NextEntryOffset = " << fileNotifyInformationHeader.NextEntryOffset
<< " Action = " << fileNotifyInformationHeader.Action
<< " FileNameLength = " << fileNotifyInformationHeader.FileNameLength;

if (fileNotifyInformationHeader.Action == FILE_ACTION_ADDED ||
fileNotifyInformationHeader.Action == FILE_ACTION_REMOVED ||
fileNotifyInformationHeader.Action == FILE_ACTION_MODIFIED ||
fileNotifyInformationHeader.Action == FILE_ACTION_RENAMED_NEW_NAME) {
onConfigFileEvent();
}
}

if (fileNotifyInformationHeader.NextEntryOffset == 0) break;
fileNotifyInformationPtr += fileNotifyInformationHeader.NextEntryOffset;
}
}

StartWatching();
}

ConfigLoader::Watcher::OverlappedWithEvent::OverlappedWithEvent() {
overlapped.hEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
if (overlapped.hEvent == NULL)
throw std::system_error(::GetLastError(), std::system_category(), "Unable to create watch event");
}
ConfigLoader::Watcher::OverlappedWithEvent::~OverlappedWithEvent() {
UniqueHandle(overlapped.hEvent);
}

void ConfigLoader::Watcher::StartWatching() {
if (::ReadDirectoryChangesW(
directory.get(),
fileNotifyInformationBuffer, sizeof(fileNotifyInformationBuffer),
/*bWatchSubtree=*/FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE,
/*lpBytesReturned=*/NULL,
&overlapped.overlapped,
/*lpCompletionRoutine=*/NULL) == 0)
throw std::system_error(::GetLastError(), std::system_category(), "Unable to watch for directory changes");
}

ConfigLoader::ConfigLoader(std::function<void()> onConfigChange) :
onConfigChange(std::move(onConfigChange)),
configDirectory(GetUserDirectory()),
initialConfig(LoadConfig(configDirectory / configFileName)) {}

void ConfigLoader::OnConfigFileEvent() {
Log() << "Handling config file event";

// TODO: handle exceptions
// TODO: do not reset if the configuration has not actually changed
LoadConfig(configDirectory / configFileName);
onConfigChange();
}

}
51 changes: 50 additions & 1 deletion src/flexasio/FlexASIO/config.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
#pragma once

#include <windows.h>

#include <array>
#include <filesystem>
#include <functional>
#include <optional>
#include <string>
#include <thread>

namespace flexasio {

Expand All @@ -21,6 +27,49 @@ namespace flexasio {
Stream output;
};

Config LoadConfig();
class ConfigLoader {
public:
ConfigLoader(std::function<void()> onConfigChange);

const Config& Initial() const { return initialConfig; }

private:
void OnConfigFileEvent();

struct HandleCloser {
void operator()(HANDLE handle);
};
using UniqueHandle = std::unique_ptr<std::remove_pointer_t<HANDLE>, HandleCloser>;

class Watcher {
public:
Watcher(std::function<void()> onConfigFileEvent, const std::filesystem::path& configDirectory);
~Watcher() noexcept(false);

private:
struct OverlappedWithEvent {
OverlappedWithEvent();
~OverlappedWithEvent();

OVERLAPPED overlapped = { 0 };
};

void StartWatching();
void RunThread();
void OnEvent();

const std::function<void()> onConfigFileEvent;
const UniqueHandle stopEvent;
const UniqueHandle directory;
OverlappedWithEvent overlapped;
alignas(DWORD) char fileNotifyInformationBuffer[64 * 1024];
std::thread thread;
};

const std::function<void()> onConfigChange;
const std::filesystem::path configDirectory;
const Watcher watcher{ [this] { OnConfigFileEvent(); }, configDirectory };
const Config initialConfig;
};

}
27 changes: 24 additions & 3 deletions src/flexasio/FlexASIO/flexasio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ namespace flexasio {

FlexASIO::FlexASIO(void* sysHandle) :
windowHandle(reinterpret_cast<decltype(windowHandle)>(sysHandle)),
config(LoadConfig()),
portAudioDebugRedirector([](std::string_view str) { if (IsLoggingEnabled()) Log() << "[PortAudio] " << str; }),
hostApi([&] {
LogPortAudioApiList();
Expand Down Expand Up @@ -658,8 +657,17 @@ namespace flexasio {
// See https://github.com/dechamps/FlexASIO/issues/31
Log() << "WARNING: ASIO host application never enquired about sample rate, and therefore cannot know we are running at " << sampleRate << " Hz!";
}

preparedState.emplace(*this, sampleRate, bufferInfos, numChannels, bufferSize, callbacks);
bool resetRequested;
{
const std::lock_guard resetRequestLock(resetRequestMutex);
preparedState.emplace(*this, sampleRate, bufferInfos, numChannels, bufferSize, callbacks);
resetRequested = this->resetRequested;
this->resetRequested = false;
}
if (resetRequested) {
Log() << "Acting on a previous reset request";
preparedState->RequestReset();
}
}

FlexASIO::PreparedState::Buffers::Buffers(size_t bufferSetCount, size_t inputChannelCount, size_t outputChannelCount, size_t bufferSizeInFrames, size_t inputSampleSizeInBytes, size_t outputSampleSizeInBytes) :
Expand Down Expand Up @@ -731,6 +739,7 @@ namespace flexasio {
void FlexASIO::DisposeBuffers()
{
if (!preparedState.has_value()) throw ASIOException(ASE_InvalidMode, "disposeBuffers() called before createBuffers()");
const std::lock_guard resetRequestLock(resetRequestMutex);
preparedState.reset();
}

Expand Down Expand Up @@ -944,6 +953,18 @@ namespace flexasio {
outputReadyCondition.notify_all();
}

void FlexASIO::RequestReset() {
Log() << "Handling reset request";

const std::lock_guard resetRequestLock(resetRequestMutex);
if (!preparedState.has_value()) {
Log() << "No prepared state, will issue reset later";
resetRequested = true;
return;
}
preparedState->RequestReset();
}

void FlexASIO::PreparedState::RequestReset() {
if (!callbacks.asioMessage || Message(callbacks.asioMessage, kAsioSelectorSupported, kAsioResetRequest, nullptr, nullptr) != 1)
throw ASIOException(ASE_InvalidMode, "reset requests are not supported");
Expand Down
8 changes: 7 additions & 1 deletion src/flexasio/FlexASIO/flexasio.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,11 @@ namespace flexasio {

OpenStreamResult OpenStream(bool inputEnabled, bool outputEnabled, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback callback, void* callbackUserData);

void RequestReset();

const HWND windowHandle = nullptr;
const Config config;
const ConfigLoader configLoader{ [this] { RequestReset(); } };
const Config& config = configLoader.Initial();

PortAudioDebugRedirector portAudioDebugRedirector;
PortAudioHandle portAudioHandle;
Expand All @@ -230,6 +233,9 @@ namespace flexasio {
bool sampleRateWasAccessed = false;
bool hostSupportsOutputReady = false;

std::mutex resetRequestMutex;
bool resetRequested = false;

std::optional<PreparedState> preparedState;
};

Expand Down

0 comments on commit 7f411c5

Please sign in to comment.