#include "main.hpp"

#include "../include/dv-sdk/cross/portable_io.h"

#include "config.hpp"
#include "config_server/config_server.h"
#include "devices_discovery.hpp"
#include "log.hpp"
#include "module.hpp"
#include "modules_discovery.hpp"

#include <boost/nowide/args.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/nowide/filesystem.hpp>
#include <boost/nowide/fstream.hpp>

#include <algorithm>
#include <chrono>
#include <csignal>
#include <iostream>
#include <mutex>

#if defined(ENABLE_CPU_PROFILER)
#	include "cpu_profiler.hpp"

static void GCPUProfilerListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	(void) userData;

	dv::Config::Node n{node};

	if (event == DVCFG_ATTRIBUTE_MODIFIED && changeType == DVCFG_TYPE_BOOL
		&& std::string(changeKey) == "cpuProfilerEnable") {
		if (changeValue.boolean) {
			if (!GCPUProfilerStart(n.getString("cpuProfilerOutputDirectory"), n.getString("cpuProfilerOutputPrefix"))) {
				// Failed to start, invalid file: disable profiling.
				n.attributeBooleanReset("cpuProfilerEnable");
			}
		}
		else {
			GCPUProfilerStop();
		}
	}
}
#endif

#if defined(ENABLE_HEAP_PROFILER)
#	include "heap_profiler.hpp"

static void GHeapProfilerListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	(void) userData;

	dv::Config::Node n{node};

	if (event == DVCFG_ATTRIBUTE_MODIFIED && changeType == DVCFG_TYPE_BOOL
		&& std::string(changeKey) == "heapProfilerEnable") {
		if (changeValue.boolean) {
			if (!GHeapProfilerStart(
					n.getString("heapProfilerOutputDirectory"), n.getString("heapProfilerOutputPrefix"))) {
				// Failed to start, invalid file: disable profiling.
				n.attributeBooleanReset("heapProfilerEnable");
			}
		}
		else {
			GHeapProfilerStop();
		}
	}
}
#endif

// If Boost version recent enough, enable better stack traces on segfault.
#include <boost/version.hpp>

#if defined(BOOST_VERSION) && (BOOST_VERSION / 100000) == 1 && (BOOST_VERSION / 100 % 1000) >= 66
#	define BOOST_HAS_STACKTRACE 1
#else
#	define BOOST_HAS_STACKTRACE 0
#endif

#if BOOST_HAS_STACKTRACE
#	define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED 1
#	include <boost/stacktrace.hpp>
#elif defined(OS_LINUX)
#	include <execinfo.h>
#endif

#if defined(OS_LINUX)
#	include <sys/sysinfo.h>
#	include <sys/types.h>
#	include <cstring>
#endif

#if defined(OS_WINDOWS)
#	define WIN32_LEAN_AND_MEAN
#	include <psapi.h>
#	include <windows.h>
#endif

#if defined(OS_MACOS)
#	include <mach/mach.h>
#endif

static void mainSegfaultHandler(int signum);
static void mainShutdownHandler(int signum);
static void systemRunningListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue);

void dv::addModule(const std::string &name, const std::string &library, bool startModule) {
	std::unique_lock lock(dv::MainData::getGlobal().modulesLock);

	const auto *restoreLogger = dv::LoggerInternal::Get();

	try {
		dv::MainData::getGlobal().modules.try_emplace(name, std::make_shared<dv::Module>(name, library));

		if (startModule) {
			dv::MainData::getGlobal().modules.at(name)->startThread();
		}
	}
	catch (const std::exception &ex) {
		dv::Log(dv::logLevel::ERROR, "addModule(): '{:s}', removing module.", ex.what());
		dv::Cfg::GLOBAL.getNode("/mainloop/" + name + "/").removeNode();
	}

	dv::LoggerInternal::Set(restoreLogger);
}

void dv::removeModule(const std::string &name) {
	// Check that module has stopped.
	auto moduleNode = dv::Cfg::GLOBAL.getNode("/mainloop/" + name + "/");

shutdownModule:
	// First we stop it ourselves to be sure and wait for isRunning=false.
	moduleNode.put<dv::CfgType::BOOL>("running", false);

	while (moduleNode.get<dv::CfgType::BOOL>("isRunning")) {
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
	}

	// Now we check again but do so while holding the modulesLock.
	// It could happen that the module we're trying to remove is
	// also trying to start up right now, due to a previous running=true;
	// it will either try after we do take the lock, meaning it
	// will fail to take it itself and notice it should exit and do so,
	// or it will take the lock and start up: if it succeeds, isRunning
	// will be true, so we check for that after we finally take the
	// lock successfully, and if it is, we do the shutdown again.
	{
		std::unique_lock lock(dv::MainData::getGlobal().modulesLock);

		if (moduleNode.get<dv::CfgType::BOOL>("isRunning")) {
			goto shutdownModule;
		}

		const auto *restoreLogger = dv::LoggerInternal::Get();

		dv::MainData::getGlobal().modules.at(name)->stopThread();

		dv::MainData::getGlobal().modules.erase(name);

		dv::LoggerInternal::Set(restoreLogger);
	}
}

// structure for storing memory usage
struct MemoryUsage {
	int64_t virtualMem;
	int64_t physicalMem;
};

#if defined(OS_LINUX)
static constexpr int SIZE_KB = 1024;

static MemoryUsage getMemoryUsageLinux() {
	MemoryUsage result{0, 0};

	auto procFile = boost::nowide::ifstream{"/proc/self/status"};

	if (!procFile.is_open()) {
		return result;
	}

	std::string procLine;
	while (std::getline(procFile, procLine)) {
		const auto delimiter = procLine.find(':');

		if (delimiter == std::string::npos) {
			continue;
		}

		const auto name  = procLine.substr(0, delimiter);
		const auto value = procLine.substr(delimiter + 1);

		if (name == "VmSize") {
			result.virtualMem = std::stoll(value);
		}

		if (name == "VmRSS") {
			result.physicalMem = std::stoll(value);
		}
	}

	// Note: this value is in KB! Convert to bytes.
	result.virtualMem  *= SIZE_KB;
	result.physicalMem *= SIZE_KB;

	return result;
}
#endif

static MemoryUsage getMemoryUsage() {
	MemoryUsage memory{0, 0};

#if defined(OS_LINUX)
	memory = getMemoryUsageLinux();
#endif

#if defined(OS_WINDOWS)
	PROCESS_MEMORY_COUNTERS_EX pmc;
	auto res
		= GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS *>(&pmc), sizeof(pmc));

	if (res) {
		memory.virtualMem  = static_cast<int64_t>(pmc.PrivateUsage);
		memory.physicalMem = static_cast<int64_t>(pmc.WorkingSetSize);
	}
#endif

#if defined(OS_MACOS)
	struct task_basic_info t_info;
	mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;

	if (task_info(mach_task_self(), TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&t_info), &t_info_count)
		== KERN_SUCCESS) {
		memory.virtualMem  = t_info.virtual_size;
		memory.physicalMem = t_info.resident_size;
	};
#endif

	return (memory);
}

static inline void setupLibraryHooks() {
	// Setup internal function pointers for public support library.
	auto *libFuncPtrs = &dv::MainData::getGlobal().libFunctionPointers;

	libFuncPtrs->getTypeInfoCharString
		= [typeSystem = &dv::MainData::getGlobal().typeSystem](const char *cs, const dv::Module *m) {
			  return (typeSystem->getTypeInfo(cs, m));
		  };
	libFuncPtrs->getTypeInfoIntegerID
		= [typeSystem = &dv::MainData::getGlobal().typeSystem](uint32_t ii, const dv::Module *m) {
			  return (typeSystem->getTypeInfo(ii, m));
		  };

	libFuncPtrs->registerType      = &dv::Module::registerType;
	libFuncPtrs->registerOutput    = &dv::Module::registerOutput;
	libFuncPtrs->registerInput     = &dv::Module::registerInput;
	libFuncPtrs->outputAllocate    = &dv::Module::outputAllocate;
	libFuncPtrs->outputCommit      = &dv::Module::outputCommit;
	libFuncPtrs->inputGet          = &dv::Module::inputGet;
	libFuncPtrs->inputAdvance      = &dv::Module::inputAdvance;
	libFuncPtrs->inputDismiss      = &dv::Module::inputDismiss;
	libFuncPtrs->outputGetInfoNode = &dv::Module::outputGetInfoNode;
	libFuncPtrs->inputGetInfoNode  = &dv::Module::inputGetInfoNode;
	libFuncPtrs->inputIsConnected  = &dv::Module::inputIsConnected;

	libFuncPtrs->log = &dv::LoggerInternal::LogInternal;

	dv::SDKLibInit(libFuncPtrs);
}

int main(int argc, char **argv) {
	// Setup internal function pointers for public support library.
	setupLibraryHooks();

	// UTF-8 support for file paths and CLI arguments on Windows.
	boost::nowide::nowide_filesystem();
	boost::nowide::args utfArgs(argc, argv);

	// Initialize config storage from file, support command-line overrides.
	dv::ConfigInit(argc, argv);

	// Initialize logging sub-system.
	dv::LoggerInternal::Init();

// Install signal handler for global shutdown. Not inherited by child after fork().
#if defined(OS_WINDOWS)
	if (signal(SIGTERM, &mainShutdownHandler) == SIG_ERR) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGTERM. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (signal(SIGINT, &mainShutdownHandler) == SIG_ERR) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGINT. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (signal(SIGBREAK, &mainShutdownHandler) == SIG_ERR) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGBREAK. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (signal(SIGSEGV, &mainShutdownHandler) == SIG_ERR) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGSEGV. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (signal(SIGABRT, &mainShutdownHandler) == SIG_ERR) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGABRT. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	// Disable closing of the console window where DV is executing.
	// While we do catch the signal (SIGBREAK) that such an action generates, it seems
	// we can't reliably shut down within the hard time window that Windows enforces when
	// pressing the close button (X in top right corner usually). This seems to be just
	// 5 seconds, and we can't guarantee full shutdown (USB, file writing, etc.) in all
	// cases within that time period (multiple cameras, modules etc. make this worse).
	// So we just disable that and force the user to CTRL+C, which works fine.
	HWND consoleWindow = GetConsoleWindow();
	if (consoleWindow != nullptr) {
		HMENU systemMenu = GetSystemMenu(consoleWindow, false);
		EnableMenuItem(systemMenu, SC_CLOSE, MF_GRAYED);
	}
#else
	struct sigaction shutdown;

	shutdown.sa_handler = &mainShutdownHandler;
	shutdown.sa_flags   = 0;
	sigemptyset(&shutdown.sa_mask);
	sigaddset(&shutdown.sa_mask, SIGTERM);
	sigaddset(&shutdown.sa_mask, SIGINT);

	if (sigaction(SIGTERM, &shutdown, nullptr) == -1) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGTERM. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (sigaction(SIGINT, &shutdown, nullptr) == -1) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGINT. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	struct sigaction segfault;

	segfault.sa_handler = &mainSegfaultHandler;
	segfault.sa_flags   = 0;
	sigemptyset(&segfault.sa_mask);
	sigaddset(&segfault.sa_mask, SIGSEGV);
	sigaddset(&segfault.sa_mask, SIGABRT);
	sigaddset(&segfault.sa_mask, SIGILL);

	if (sigaction(SIGSEGV, &segfault, nullptr) == -1) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGSEGV. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (sigaction(SIGABRT, &segfault, nullptr) == -1) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGABRT. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	if (sigaction(SIGILL, &segfault, nullptr) == -1) {
		dv::Log(dv::logLevel::ERROR, "Failed to set signal handler for SIGILL. Error: {:d}.", errno);
		exit(EXIT_FAILURE);
	}

	// Ignore SIGPIPE.
	signal(SIGPIPE, SIG_IGN);
#endif

	// Ensure core nodes exist.
	auto systemNode   = dv::Cfg::GLOBAL.getNode("/system/");
	auto mainloopNode = dv::Cfg::GLOBAL.getNode("/mainloop/");

	// Add core attributes.
	systemNode.create<dv::CfgType::STRING>("version", std::string{dv::runtime::VERSION_STRING}, {0, 32},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Runtime version.");

	mainloopNode.attributeModifierGUISupport();

	systemNode.create<dv::CfgType::STRING>("user", dv::portable_get_user_name(), {0, 4096},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "User DV is running as.");

	// Support device discovery.
	auto devicesNode = systemNode.getRelativeNode("devices/");

	devicesNode.create<dv::CfgType::BOOL>("updateAvailableDevices", false, {},
		dv::CfgFlags::NORMAL | dv::CfgFlags::NO_EXPORT, "Update available devices list.");
	devicesNode.attributeModifierButton("updateAvailableDevices", "Update");
	devicesNode.addAttributeListener(nullptr, &dv::DevicesUpdateListener);

	// Initialize module related configuration.
	auto modulesNode = systemNode.getRelativeNode("modules/");

	// Additional modules search directories.
	const auto *modulesEnvVar = boost::nowide::getenv("DV_MODULES_PATH");
	modulesNode.create<dv::CfgType::STRING>("modulesSearchPath", (modulesEnvVar == nullptr) ? ("") : (modulesEnvVar),
		{0, 64 * PATH_MAX}, dv::CfgFlags::NORMAL,
		"Additional directories to search loadable modules in, separated by '|'.");

	modulesNode.create<dv::CfgType::BOOL>("updateModulesInformation", false, {},
		dv::CfgFlags::NORMAL | dv::CfgFlags::NO_EXPORT, "Update modules information.");
	modulesNode.attributeModifierButton("updateModulesInformation", "Update");
	modulesNode.addAttributeListener(nullptr, &dv::ModulesUpdateInformationListener);

	dv::ModulesUpdateInformation(); // Run once at startup.

	dv::DevicesUpdateList(); // Run once at startup, after updating modules information (needs hooks).

	// System running control.
	if (systemNode.getBool("systemService")) {
		systemNode.create<dv::CfgType::BOOL>(
			"running", true, {}, dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Global system status.");
	}
	else {
		systemNode.create<dv::CfgType::BOOL>(
			"running", true, {}, dv::CfgFlags::NORMAL | dv::CfgFlags::NO_EXPORT, "Global system status/start/stop.");
		systemNode.addAttributeListener(nullptr, &systemRunningListener);
	}

	// Virtual and physical memory usage of the process
	systemNode.create<dv::CfgType::LONG>("virtualMemory", 0, {0, INT64_MAX},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Virtual memory used by the process.");
	systemNode.create<dv::CfgType::LONG>("physicalMemory", 0, {0, INT64_MAX},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Physical memory used by the process.");

	// Start the configuration server thread for run-time config changes.
	ConfigServer cfgServer{};

	try {
		// Open listening socket, configure TLS.
		cfgServer.serviceConfigure();
	}
	catch (const boost::system::system_error &) {
		return EXIT_FAILURE;
	}

	// Add each module defined in configuration to runnable modules.
	// Do not start them yet.
	for (const auto &child : mainloopNode.getChildren()) {
		dv::addModule(child.getName(), child.get<dv::CfgType::STRING>("moduleLibrary"), false);
	}

	// Start profilers, if enabled, after modules have been added, so that
	// the libraries are present in memory.
#if defined(ENABLE_CPU_PROFILER)
	auto cpuProfilerNode = systemNode.getRelativeNode("profilers/");

	cpuProfilerNode.create<dv::CfgType::STRING>("cpuProfilerOutputDirectory", dv::portable_get_user_home_directory(),
		{0, 64 * PATH_MAX}, dv::CfgFlags::NORMAL, "Directory in which to write CPU profiling information.");
	cpuProfilerNode.attributeModifierFileChooser("cpuProfilerOutputDirectory", "DIRECTORY");
	cpuProfilerNode.create<dv::CfgType::STRING>("cpuProfilerOutputPrefix", "dv_prof_cpu", {0, 128},
		dv::CfgFlags::NORMAL, "Prefix for CPU profiling file name.");
	cpuProfilerNode.create<dv::CfgType::BOOL>(
		"cpuProfilerEnable", false, {}, dv::CfgFlags::NORMAL, "Enable CPU profiling.");
	cpuProfilerNode.addAttributeListener(nullptr, &GCPUProfilerListener);

	if (cpuProfilerNode.getBool("cpuProfilerEnable")) {
		if (!GCPUProfilerStart(cpuProfilerNode.getString("cpuProfilerOutputDirectory"),
				cpuProfilerNode.getString("cpuProfilerOutputPrefix"))) {
			// Failed to start, invalid file: disable profiling.
			cpuProfilerNode.putBool("cpuProfilerEnable", false);
		}
	}
#endif

#if defined(ENABLE_HEAP_PROFILER)
	auto heapProfilerNode = systemNode.getRelativeNode("profilers/");

	heapProfilerNode.create<dv::CfgType::STRING>("heapProfilerOutputDirectory", dv::portable_get_user_home_directory(),
		{0, 64 * PATH_MAX}, dv::CfgFlags::NORMAL, "Directory in which to write heap profiling information.");
	heapProfilerNode.attributeModifierFileChooser("heapProfilerOutputDirectory", "DIRECTORY");
	heapProfilerNode.create<dv::CfgType::STRING>("heapProfilerOutputPrefix", "dv_prof_mem", {0, 128},
		dv::CfgFlags::NORMAL, "Prefix for heap profiling file name.");
	heapProfilerNode.create<dv::CfgType::BOOL>(
		"heapProfilerEnable", false, {}, dv::CfgFlags::NORMAL, "Enable heap profiling.");
	heapProfilerNode.addAttributeListener(nullptr, &GHeapProfilerListener);

	if (heapProfilerNode.getBool("heapProfilerEnable")) {
		if (!GHeapProfilerStart(heapProfilerNode.getString("heapProfilerOutputDirectory"),
				heapProfilerNode.getString("heapProfilerOutputPrefix"))) {
			// Failed to start, invalid file: disable profiling.
			heapProfilerNode.putBool("heapProfilerEnable", false);
		}
	}
#endif

	// Start modules after having added them all, so that connections between
	// each other may correctly resolve right away.
	{
		std::unique_lock lock(dv::MainData::getGlobal().modulesLock);

		for (const auto &m : dv::MainData::getGlobal().modules) {
			m.second->startThread();
		}
	}

	// Enable config server.
	cfgServer.threadStart();

	// Main thread now works as updater (sleeps most of the time).
	size_t systemSleepCounter = 0;

	while (dv::MainData::getGlobal().systemRunning) {
		if (systemSleepCounter == (100 - 1)) {
			systemSleepCounter = 0;

			dv::Cfg::GLOBAL.attributeUpdaterRun();

			const auto procMem = getMemoryUsage();
			systemNode.updateReadOnly<dv::CfgType::LONG>("virtualMemory", procMem.virtualMem);
			systemNode.updateReadOnly<dv::CfgType::LONG>("physicalMemory", procMem.physicalMem);
		}

		// Check running again before sleeping, as code above
		// could take some time to complete.
		if (dv::MainData::getGlobal().systemRunning) {
			std::this_thread::sleep_for(std::chrono::milliseconds(10));

			systemSleepCounter++;
		}
	}

	dv::Cfg::GLOBAL.attributeUpdaterRemoveAll();

	// After shutting down the updater, also shutdown the config server thread,
	// to ensure no more changes can happen.
	cfgServer.threadStop();

	if (systemNode.getBool("systemService")) {
		// Write config back on shutdown of service, after config server is disabled (no more
		// changes), again, but before we set running to false on all modules and force
		// their shutdown (so that correct run status is written to file).
		systemNode.getRelativeNode("config/").putBool("writeSaveFile", true);
	}

	// Separate input modules from others.
	std::vector<dv::Cfg::Node> inputModules;
	std::vector<dv::Cfg::Node> normalModules;

	for (auto &child : mainloopNode.getChildren()) {
		if (child.existsRelativeNode("inputs/") && !child.getRelativeNode("inputs/").getChildren().empty()) {
			normalModules.push_back(child);
		}
		else {
			inputModules.push_back(child);
		}
	}

	// Terminate all normal modules.
	for (auto &child : normalModules) {
		child.put<dv::CfgType::BOOL>("running", false);
	}

	// Terminate all input modules.
	for (auto &child : inputModules) {
		child.put<dv::CfgType::BOOL>("running", false);
	}

	// Stop profilers, if enabled, before removing modules, so that
	// the libraries are still loaded.
#if defined(ENABLE_CPU_PROFILER)
	if (cpuProfilerNode.getBool("cpuProfilerEnable")) {
		GCPUProfilerStop();
	}
#endif

#if defined(ENABLE_HEAP_PROFILER)
	if (heapProfilerNode.getBool("heapProfilerEnable")) {
		GHeapProfilerStop();
	}
#endif

	// Remove all normal modules.
	for (auto &child : normalModules) {
		dv::removeModule(child.getName());
	}

	// Remove all input modules.
	for (auto &child : inputModules) {
		dv::removeModule(child.getName());
	}

	// Remove attribute listeners for clean shutdown.
	systemNode.removeAttributeListener(nullptr, &systemRunningListener);
	modulesNode.removeAttributeListener(nullptr, &dv::ModulesUpdateInformationListener);
	devicesNode.removeAttributeListener(nullptr, &dv::DevicesUpdateListener);

	return EXIT_SUCCESS;
}

static void mainSegfaultHandler(int signum) {
	signal(signum, SIG_DFL);

// Segfault or abnormal termination, try to print a stack trace if possible.
#if BOOST_HAS_STACKTRACE
	std::cerr << boost::stacktrace::stacktrace() << std::endl << std::flush;
#elif defined(OS_LINUX)
	void *traces[128];
	int tracesActualNum = backtrace(traces, 128);
	backtrace_symbols_fd(traces, tracesActualNum, STDERR_FILENO);
#endif

	raise(signum);
}

static void mainShutdownHandler(int signum) {
	UNUSED_ARGUMENT(signum);

	// Set the system running flag to false on SIGTERM and SIGINT (CTRL+C) for global shutdown.
	dv::MainData::getGlobal().systemRunning = false;
}

static void systemRunningListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	UNUSED_ARGUMENT(node);
	UNUSED_ARGUMENT(userData);
	UNUSED_ARGUMENT(changeValue);

	if (event == DVCFG_ATTRIBUTE_MODIFIED && changeType == DVCFG_TYPE_BOOL && std::string(changeKey) == "running") {
		// Set the system running flag to false for global shutdown.
		dv::MainData::getGlobal().systemRunning = false;
	}
}
