diff --git a/.vscode/launch.json b/.vscode/launch.json index 51dfddb..536ea90 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,9 @@ "request": "launch", // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", - "args": [ ], + "args": [ + //"-h" + ], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [ diff --git a/CMakeLists.txt b/CMakeLists.txt index 21cf7aa..f0deb4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,11 @@ set (CMAKE_INSTALL_PREFIX "/usr/") include(CTest) enable_testing() + + + + + #add_subdirectory("submodules/pipedal_p2pd") add_subdirectory("PiPedalCommon") diff --git a/PiPedalCommon/src/SysExec.cpp b/PiPedalCommon/src/SysExec.cpp index 8506b34..2244cf3 100644 --- a/PiPedalCommon/src/SysExec.cpp +++ b/PiPedalCommon/src/SysExec.cpp @@ -306,3 +306,37 @@ SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::s } } + + +int pipedal::SudoExec(int argc, char **argv) +{ + // re-execute with SUDO in order to prompt for SUDO credentials once only. + std::vector args; + std::string pkexec = "/usr/bin/sudo"; // staged because "ISO C++ forbids converting a string constant to std::vector::value_type"(!) + args.push_back((char *)(pkexec.c_str())); + + std::string sPath = getSelfExePath(); + args.push_back(const_cast(sPath.c_str())); + for (int arg = 1; arg < argc; ++arg) + { + args.push_back(const_cast(argv[arg])); + } + + args.push_back(nullptr); + + char **newArgs = &args[0]; + + int pbPid; + int returnValue = 0; + + if ((pbPid = fork()) == 0) + { + return execv(newArgs[0], newArgs); + } + else + { + waitpid(pbPid, &returnValue, 0); + int exitStatus = WEXITSTATUS(returnValue); + return exitStatus; + } +} diff --git a/PiPedalCommon/src/include/SysExec.hpp b/PiPedalCommon/src/include/SysExec.hpp index 031b90d..7b0a1d2 100644 --- a/PiPedalCommon/src/include/SysExec.hpp +++ b/PiPedalCommon/src/include/SysExec.hpp @@ -43,5 +43,9 @@ namespace pipedal void sysExecTerminate(ProcessId pid_, int termTimeoutMs = 1000, int killTimeoutMs = 500); // returns the process's exit status. int sysExecWait(ProcessId pid); + // re-execute the current program with current argument, but with sudo. + int SudoExec(int argc, char **argv); + + std::string getSelfExePath(); } \ No newline at end of file diff --git a/a.out b/a.out new file mode 100644 index 0000000..dd7982f Binary files /dev/null and b/a.out differ diff --git a/src/BootConfig.cpp b/src/BootConfig.cpp new file mode 100644 index 0000000..d70d0c2 --- /dev/null +++ b/src/BootConfig.cpp @@ -0,0 +1,329 @@ +// Copyright (c) 2024 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 AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "BootConfig.hpp" +#include +#include +#include "ss.hpp" +#include +#include "SysExec.hpp" +#include "util.hpp" +#include +#include + +namespace fs = std::filesystem; + +#include +#include +#include +#include +#include +#include + +using namespace pipedal; + +static bool IsGrubBootLoader() +{ + const std::vector grub_config_paths = { + "/boot/grub/grub.cfg", + "/boot/grub2/grub.cfg", + }; + + for (const auto &path : grub_config_paths) + { + if (std::filesystem::exists(path)) + { + return true; + } + } + return false; +} + +static std::set KNOWN_KERNEL_TYPES = { + "PREEMPT_NONE", + "PREEMPT_VOLUNTARY", + "PREEMPT", + "PREEMPT_RT", + "PREEMPT_DYNAMIC"}; + +std::string GetKernelType() +{ + auto result = sysExecForOutput("uname", "-a"); + if (result.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error("Unable to execute 'uname -a'"); + } + std::vector items = split(result.output, ' '); + for (std::string &item : items) + { + if (KNOWN_KERNEL_TYPES.contains(item)) + { + return item; + } + } + return "Unknown"; +} + +std::string ReadFileLine(const fs::path &path) +{ + if (!fs::exists(path)) + { + throw std::runtime_error(SS("File does not exist: " << path )); + } + + std::ifstream f(path); + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't read file " << path)); + } + std::string line; + + std::getline(f, line); + return line; +} +std::vector ReadFileArgs(const fs::path&path) +{ + std::string line = ReadFileLine(path); + + return split(line,' '); +} + +using namespace pipedal; +BootConfig::BootConfig() +{ + if (fs::exists("/boot/firmware/cmdline.txt")) + { + this->bootLoader = BootLoaderT::UBoot; + } + else if (IsGrubBootLoader()) + { + this->bootLoader = BootLoaderT::Grub; + } + else + { + this->bootLoader = BootLoaderT::Unknown; + } + + this->kernelType = GetKernelType(); + + if (this->kernelType == "PREEMPT_DYNAMIC") + { + this->dynamicScheduler = DynamicSchedulerT::Voluntary; // the default. + } + + std::string cmdLine = ReadFileLine("/proc/cmdline"); + std::vector cmdLineArgs = split(cmdLine, ' '); + + for (const auto &arg : cmdLineArgs) + { + if (arg == "preempt=full") + { + this->dynamicScheduler = DynamicSchedulerT::Full; + } + else if (arg == "preempt=voluntary") + { + this->dynamicScheduler = DynamicSchedulerT::Voluntary; + } + else if (arg == "preempt=none") + { + this->dynamicScheduler = DynamicSchedulerT::None; + } + else if (arg.starts_with("preempt=")) + { + this->dynamicScheduler = DynamicSchedulerT::Unknown; + } + else if (arg == "threadirqs") + { + this->threadedIrqs = true; + } + } + this->canSetThreadIrqs = this->kernelType == "PREEMPT" || this->kernelType == "PREEMPT_DYNAMIC"; +} + +BootConfig::~BootConfig() +{ + this->thread = nullptr; // join and clean up the tread before deleting ANYTHING else. +} +bool BootConfig::operator==(BootConfig &other) const +{ + return this->bootLoader == other.bootLoader && this->kernelType == other.kernelType && this->preemptMode == other.preemptMode && this->threadedIrqs == other.threadedIrqs; +} + +bool BootConfig::CanWriteConfig() +{ + if (!CanSetThreadIrqs() && dynamicScheduler == DynamicSchedulerT::NotApplicable) + { + return true; + } + return (this->bootLoader != BootLoaderT::Unknown); +} + +void BootConfig::WriteConfiguration(std::function onComplete) +{ + this->thread = nullptr; // wait for any previous operations to complete. + if (!changed) + { + if (onComplete) onComplete(true, ""); + } + else + { + changed = false; + + if (this->bootLoader == BootLoaderT::UBoot) + { + try + { + WriteUBootConfiguration(onComplete); + } + catch (const std::exception &e) + { + if (onComplete) onComplete(false, e.what()); + } + } + else if (this->bootLoader == BootLoaderT::Grub) + { + WriteGrubConfiguration(onComplete); + } + else + { + if (onComplete) onComplete(false, "Unsupported bootloader."); + } + } +} + +static std::string commandLineOption(BootConfig::DynamicSchedulerT dynamicScheduler) +{ + switch (dynamicScheduler) + { + case BootConfig::DynamicSchedulerT::None: + return "preempt=none"; + case BootConfig::DynamicSchedulerT::Voluntary: + return "preempt=voluntary"; + case BootConfig::DynamicSchedulerT::Full: + return "preempt=full"; + default: + throw std::range_error("Invalid value."); + + } +} +void BootConfig::WriteUBootConfiguration(std::function onComplete) +{ + this->thread = nullptr; // blocks if already running. + + this->thread = std::make_unique( + [this, onComplete]() + { + + try + { + fs::path cmdlineFile = "/boot/firmware/cmdline.txt"; + std::vector bootArgs = ReadFileArgs(cmdlineFile); + + std::vector newBootArgs; + for (const auto&value: bootArgs) + { + if (value != "threadirqs" && !value.starts_with("preempt=")) + { + newBootArgs.push_back(value); + } + } + if (this->canSetThreadIrqs && this->threadedIrqs) + { + newBootArgs.push_back("threadirqs"); + } + if (this->dynamicScheduler != DynamicSchedulerT::NotApplicable) + { + newBootArgs.push_back(commandLineOption(this->dynamicScheduler)); + } + std::stringstream ss; + bool firstArg = true; + for (const std::string&arg: newBootArgs) { + if (!firstArg) { + ss << ' '; + } + firstArg = false; + ss << arg; + } + ss << '\n'; + + std::string newCmdLine = ss.str(); + + { + std::ofstream f(cmdlineFile); + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't write to " << cmdlineFile)); + } + f << newCmdLine; + } + + if (onComplete) + { + onComplete(true, ""); + } + } + catch (const std::exception &e) + { + if (onComplete) + { + onComplete(false, e.what()); + } + } + }); +} +void BootConfig::WriteGrubConfiguration(std::function onComplete) +{ + this->thread = nullptr; // blocks if already running. + + this->thread = std::make_unique( + [this, onComplete]() + { + sleep(1); + + if (onComplete) + { + onComplete(false, "Grub handler not implemeneted."); + } + }); +} + +void BootConfig::DynamicScheduler(DynamicSchedulerT value) +{ + if (value != this->dynamicScheduler) + { + if (this->dynamicScheduler == DynamicSchedulerT::NotApplicable) + { + throw std::runtime_error("Illegal operation."); + } + this->dynamicScheduler = value; + this->changed = true; + } +} +void BootConfig::ThreadedIrqs(bool value) +{ + if (value != threadedIrqs) + { + if (!CanSetThreadIrqs()) + { + throw std::runtime_error("Illegal operation."); + } + this->threadedIrqs = value; + this->changed = true; + } +} diff --git a/src/BootConfig.hpp b/src/BootConfig.hpp new file mode 100644 index 0000000..c6834b0 --- /dev/null +++ b/src/BootConfig.hpp @@ -0,0 +1,83 @@ +// Copyright (c) 2024 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 AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +#pragma once + +#include +#include +#include +#include + +namespace pipedal { + class BootConfig { + public: + BootConfig(); + ~BootConfig(); + + enum class BootLoaderT { + UBoot, + Grub, + Unknown + }; + + enum class DynamicSchedulerT { + None = 0, + Voluntary = 1, + Full = 2, + + NotApplicable = -1, + Unknown = -2, + + }; + + // BEWARE: The onComplete function gets called from off the main thread on machines with GRUB bootloaders! + void WriteConfiguration(std::function onComplete); + BootLoaderT BootLoader() const { return bootLoader; } + const std::string&KernelType() const { return kernelType; } + + bool CanSetThreadIrqs() const { return canSetThreadIrqs; } + bool ThreadedIrqs() const { return threadedIrqs; } + void ThreadedIrqs(bool value); + + const DynamicSchedulerT DynamicScheduler() const { return dynamicScheduler; } + void DynamicScheduler(DynamicSchedulerT value); + + + bool CanWriteConfig(); + + bool Changed() const { return changed; } + + bool operator==(BootConfig&other) const; + private: + std::unique_ptr thread; + + void WriteUBootConfiguration(std::function onComplete); + void WriteGrubConfiguration(std::function onComplete); + + bool changed = false; + BootLoaderT bootLoader = BootLoaderT::Unknown; + std::string kernelType; + std::string preemptMode; + DynamicSchedulerT dynamicScheduler = DynamicSchedulerT::NotApplicable; + bool canSetThreadIrqs = false; + bool threadedIrqs = false; + + }; +}; \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dfe1741..749a7ea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,29 @@ set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 set(CXX_STANDARD 20) + +include(FetchContent) + +cmake_policy(SET CMP0169 OLD) + +set(FETCHCONTENT_QUIET ON) +set(FTXUI_QUIET ON) + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v5.0.0 +) + +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +message(STATUS "*** ${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR}") + + + include(FindPkgConfig) find_package(sdbus-c++ REQUIRED) @@ -303,6 +326,21 @@ if(${USE_PCH}) endif() +################################# + +add_executable(pipedal_kconfig + kconfigMain.cpp + BootConfig.cpp BootConfig.hpp + +) + +target_link_libraries(pipedal_kconfig + PRIVATE ftxui::screen + PRIVATE ftxui::dom + PRIVATE ftxui::component # Not needed for this example. + PRIVATE PiPedalCommon +) + ################################# add_executable(pipedald asan_options.cpp # disable leak checking for sanitize=address. diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 0e3d13e..11b9e69 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -561,38 +561,6 @@ void InstallAudioService() #endif } -int SudoExec(int argc, char **argv) -{ - // re-execute with SUDO in order to prompt for SUDO credentials once only. - std::vector args; - std::string pkexec = "/usr/bin/sudo"; // staged because "ISO C++ forbids converting a string constant to std::vector::value_type"(!) - args.push_back((char *)(pkexec.c_str())); - - std::string sPath = getSelfExePath(); - args.push_back(const_cast(sPath.c_str())); - for (int arg = 1; arg < argc; ++arg) - { - args.push_back(const_cast(argv[arg])); - } - - args.push_back(nullptr); - - char **newArgs = &args[0]; - - int pbPid; - int returnValue = 0; - - if ((pbPid = fork()) == 0) - { - return execv(newArgs[0], newArgs); - } - else - { - waitpid(pbPid, &returnValue, 0); - int exitStatus = WEXITSTATUS(returnValue); - return exitStatus; - } -} static bool IsP2pServiceEnabled() { diff --git a/src/Locale.cpp b/src/Locale.cpp index 9859951..8b56117 100644 --- a/src/Locale.cpp +++ b/src/Locale.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2024 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 diff --git a/src/PrettyPrinter.hpp b/src/PrettyPrinter.hpp index 284f21e..e8c12f1 100644 --- a/src/PrettyPrinter.hpp +++ b/src/PrettyPrinter.hpp @@ -183,7 +183,12 @@ namespace pipedal { ++startOfOverflow; } - std::string overflow = string(&lineBuffer[startOfOverflow], lineBuffer.size() - startOfOverflow); + + std::string overflow; + size_t extra = lineBuffer.size() - startOfOverflow; + if (extra) { + overflow = std::string(&lineBuffer[startOfOverflow], extra); + } lineBuffer.resize(breakPos); WriteLine(); diff --git a/src/kconfigMain.cpp b/src/kconfigMain.cpp new file mode 100644 index 0000000..937a79e --- /dev/null +++ b/src/kconfigMain.cpp @@ -0,0 +1,710 @@ +// Copyright (c) 2024 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 AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* + Preamble: + + The choice of backend for this project was unfortunate. The ftxui project has ungodly horrible problems + dealing with mouse and keyboard focus. Really embarassingly horrible problems. And no real sensible + support for modal dialogs. As well as deficiences in keyboard handling (no way to intercept the ESC key, for example, + which would have been a nice feature). + + So the code that follows is .... unpleasant, as it does its best to work around severe problesm and + deficiencies in FTXUI. + + Tempting as it is to rewrite from scratch, FTXUI *probably* deals with unpleasant platform issues and + complications in ncurses, which may not be entirely straighforward on windows. The tab/nav/cursor implementation + is also *almost* nice (although being able to use the enter key for OK in dialogs would have been nice, and ESC for + cancel). + + Things to watch out for: Focus handling in the FTXUI RadioBox control was unsalvageable. This code hacks together + a replacement implementation using Checkboxes instead. Checkbox focus handling was also broken, but is (just barely) + salageable. Also broken under window resizing. (It recovers when you mose over). + + Nevertheless, FTXUI is truly awful. Do not use this package. You have been warned. +*/ + +#undef _GLIBCXX_DEBUG + +#include "ftxui/component/captured_mouse.hpp" // for ftxui +#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical +#include "ftxui/component/component_base.hpp" // for ComponentBase +#include "ftxui/component/component_options.hpp" // for InputOption +#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive +#include "ftxui/dom/elements.hpp" // for text, hbox, separator, Element, operator|, vbox, border +#include "ftxui/util/ref.hpp" // for Ref +#include "ftxui/component/animation.hpp" +#include +#include +#include +#include "BootConfig.hpp" +#include +#include "CommandLineParser.hpp" +#include "PrettyPrinter.hpp" +#include "SysExec.hpp" + + +using namespace ftxui; +using Clock = ftxui::animation::Clock; +using namespace pipedal; + +std::string spinnerText(const Clock::time_point &startTime) +{ + using namespace std::chrono; + + constexpr int MAX_LENGTH = 40; + constexpr int N_DOTS = 3; + + int64_t ms = std::chrono::duration_cast(Clock::now() - startTime).count(); + + int64_t position = (ms * MAX_LENGTH / 3000) % (MAX_LENGTH + N_DOTS); + + char result[MAX_LENGTH - 1]; + for (int64_t i = 0; i < MAX_LENGTH; ++i) + { + if (i >= position && i < position + N_DOTS) + { + result[i] = '.'; + } + else + { + result[i] = ' '; + } + } + result[MAX_LENGTH] = '\0'; + return result; +} + +RadioboxOption CustomRadioboxOption(int *value, int *forceFocus) +{ + RadioboxOption option = RadioboxOption::Simple(); + if (*forceFocus >= 0) + { + // option.focused_entry = *forceFocus; + *forceFocus = -1; + } + option.on_change = [value, forceFocus]() mutable + { + *forceFocus = *value; + }; + + option.transform = [](const EntryState &s) -> Element + { +#if 0 + // Microsoft terminal do not use fonts able to render properly the default + // radiobox glyph. + auto prefix = text(s.state ? "(*) " : "( ) "); // NOLINT +#else + auto prefix = text(s.state ? "◉ " : "○ "); // NOLINT +#endif + auto t = text(s.label); + if (s.active != s.focused) + { + s.label; + } + if (s.active) + { + t = t | color(Color::Red); + } + if (s.focused) + { + t = t | bgcolor(Color::GrayDark); + } + + return hbox({prefix, t}); + }; + return option; +} +CheckboxOption CustomCheckboxOption(bool *forceFocus) +{ + CheckboxOption option = CheckboxOption::Simple(); + + option.on_change = [forceFocus]() mutable + { + *forceFocus = true; + }; + option.transform = [](const EntryState &s) -> Element + { +#if 0 + // Microsoft terminal do not use fonts able to render properly the default + // radiobox glyph. + auto prefix = text(s.state ? "[X] " : "[ ] "); // NOLINT +#else + auto prefix = text(s.state ? "▣ " : "☐ "); // NOLINT +#endif + auto t = text(s.label); + if (s.active != s.focused) + { + s.label; + } + if (s.active) + { + t = t | color(Color::White); + } + if (s.focused) + { + t = t | color(Color::White) | bgcolor(Color::GrayDark); + } + + return hbox({prefix, t}); + }; + return option; +} +CheckboxOption CustomRadioCheckboxOption(bool *selections, size_t size, size_t index, int *forceFocus) +{ + CheckboxOption option = CheckboxOption::Simple(); + + option.on_change = [selections, size, index, forceFocus]() mutable + { + *forceFocus = index; + for (size_t i = 0; i < size; ++i) + { + selections[i] = (i == index); + } + }; + option.transform = [](const EntryState &s) -> Element + { +#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK) + // Microsoft terminal do not use fonts able to render properly the default + // radiobox glyph. + auto prefix = text(s.state ? "(*) " : "( ) "); // NOLINT +#else + auto prefix = text(s.state ? "◉ " : "○ "); // NOLINT +#endif + auto t = text(s.label); + if (s.active != s.focused) + { + s.label; + } + if (s.active) + { + t = t | color(Color::White); + } + if (s.focused) + { + t = t | color(Color::White) | bgcolor(Color::GrayDark); + } + + return hbox({prefix, t}); + }; + return option; +} + +std::unique_ptr setBootParametersThread; + +void setBootParameters(std::shared_ptr newConfig, std::function onComplete) +{ + newConfig->WriteConfiguration(onComplete); +} + +void kconfigUi(void) +{ + const std::string kernelType = "PREEMPT_DYNAMIC"; + + int kernelMode = 0; + + const int NO_DIALOG = 0; + const int PROCESSING_DIALOG = 1; + const int REBOOT_DIALOG = 2; + const int ERROR_DIALOG = 3; + + Clock::time_point startTime = Clock::now(); + + int dialogIndex = 0; + std::string errorMessage; + + auto screen = ScreenInteractive::Fullscreen(); + + auto quit = [&screen]() + { + screen.Exit(); + }; + + std::mutex updateCallbackMutex; + bool updatePending = false; + bool updateComplete = false; + std::string updateErrorMessage; + + auto onError = [&errorMessage, &dialogIndex](const std::string &error) mutable + { + errorMessage = error; + dialogIndex = ERROR_DIALOG; + }; + + bool useThreadedIrqs = true; + + BootConfig::DynamicSchedulerT dynamicScheduler = BootConfig::DynamicSchedulerT::NotApplicable; + bool threadedIrqsAvailable = false; + bool configRequired = false; + + std::unique_ptr bootConfig; + using DynamicSchedulerT = BootConfig::DynamicSchedulerT; + try + { + bootConfig = std::make_unique(); + threadedIrqsAvailable = bootConfig->CanSetThreadIrqs(); + dynamicScheduler = bootConfig->DynamicScheduler(); + useThreadedIrqs = bootConfig->ThreadedIrqs(); + configRequired = bootConfig->DynamicScheduler() != DynamicSchedulerT::NotApplicable || useThreadedIrqs; + + if (!bootConfig->CanWriteConfig()) + { + throw std::runtime_error("Unrecognized boot loader. Unable to reconfigure the kernel."); + } + } + catch (const std::exception &e) + { + onError(e.what()); + } + + bool forceThreadedIrqFocus = false; + + + int dynamicSchedulerIndex = (int)dynamicScheduler; + std::vector preemptModeLabelRefs = { + " none ", + " voluntary ", + " full (recommended) "}; + bool preemptModeSelections[3]; // std::vector is bizarrely specialized, so it doesn't wrok. + + + for (int i = 0; i < 3; ++i) + { + preemptModeSelections[i] = (i == dynamicSchedulerIndex); + } + + + + int forceSchedulerFocus = -1; + + Component preemptCheckBox0 = + Checkbox( + preemptModeLabelRefs[0], + &(preemptModeSelections[0]), + CustomRadioCheckboxOption(preemptModeSelections, 3, 0, &forceSchedulerFocus)); + Component preemptCheckBox1 = + Checkbox( + preemptModeLabelRefs[1], + &(preemptModeSelections[1]), + CustomRadioCheckboxOption(preemptModeSelections, 3, 1, &forceSchedulerFocus)); + Component preemptCheckBox2 = + Checkbox( + preemptModeLabelRefs[2], + &(preemptModeSelections[2]), + CustomRadioCheckboxOption(preemptModeSelections, 3, 2, &forceSchedulerFocus)); + + std::vector preemptCheckboxes{ + preemptCheckBox0, + preemptCheckBox1, + preemptCheckBox2, + }; + + ConstStringRef checkboxRef{" Enable threadirqs (recommended) "}; + Component threadedIrqCheckbox = Checkbox(checkboxRef, &useThreadedIrqs, CustomCheckboxOption(&forceThreadedIrqFocus)); + + // The basic input components: + auto handleSave = [&]() + { + if (updatePending) + { + onError("Save already running."); + return; + } + + dialogIndex = PROCESSING_DIALOG; + + std::shared_ptr newBootConfig = std::make_shared(); + + if (bootConfig->CanSetThreadIrqs()) + { + newBootConfig->ThreadedIrqs(useThreadedIrqs); + } + if (bootConfig->DynamicScheduler() != DynamicSchedulerT::NotApplicable) + { + int schedulerIndex = -1; + for (size_t i = 0; i < 3; ++i) + { + if (preemptModeSelections[i]) + { + schedulerIndex = i; + break; + } + } + if (schedulerIndex == -1) + { + onError("Internal state error."); + } + else + { + newBootConfig->DynamicScheduler((DynamicSchedulerT)schedulerIndex); + + if (newBootConfig->Changed()) + { + { + std::lock_guard lock{updateCallbackMutex}; + updatePending = true; + updateComplete = false; + updateErrorMessage = true; + } + + newBootConfig->WriteConfiguration( + [&](bool success, std::string errorMessage) + { + // potentially wrong-threaded. We pick up the result because we're animating the "..." + std::lock_guard lock(updateCallbackMutex); + updateComplete = true; + updateErrorMessage = errorMessage; + }); + } else { + // no changes. Just quit. + quit(); + } + } + } + }; + + Component okButton = Button( + ConstStringRef(" OK "), + [&]() mutable + { + screen.ExitLoopClosure()(); + }, + ButtonOption::Ascii()); + + Component saveButton = Button( + ConstStringRef(" Save "), + [&]() mutable + { + handleSave(); + }, + ButtonOption::Ascii()); + + Component cancelButton = Button( + ConstStringRef(" Cancel "), + [&screen]() mutable + { + screen.ExitLoopClosure()(); + }, + ButtonOption::Ascii()); + + // The component tree: + Component tabOrder; + + if (configRequired) + { + Components components; + if (dynamicScheduler != DynamicSchedulerT::NotApplicable) + { + components.push_back(preemptCheckBox0); + components.push_back(preemptCheckBox1); + components.push_back(preemptCheckBox2); + } + if (threadedIrqsAvailable) + { + components.push_back(threadedIrqCheckbox); + } + components.push_back(Container::Horizontal({cancelButton, saveButton})); + + tabOrder = Container::Vertical(components); + } + else + { + tabOrder = Container::Vertical({okButton}); + } + + // component tree for Processing dialog. + auto focusTrap = Container::Horizontal({Button(ButtonOption::Ascii())}); + + auto mainRenderer = Renderer( + tabOrder, + [&]() mutable + { + if (forceThreadedIrqFocus) + { + threadedIrqCheckbox->TakeFocus(); + forceThreadedIrqFocus = false; + } + if (forceSchedulerFocus >= 0) + { + preemptCheckboxes[forceSchedulerFocus]->TakeFocus(); + forceSchedulerFocus = -1; + } + Elements topLevelElements; + + Elements contentElements; + { + contentElements.push_back(hbox(text("Kernel type: "), text(kernelType))); + contentElements.push_back(text("")); + + if (!configRequired) + { + contentElements.push_back(text("No configuration required.")); + } + else + { + Elements kernelCommandLineOptions; + { + kernelCommandLineOptions.push_back(text("")); + if (dynamicScheduler != DynamicSchedulerT::NotApplicable) + { + kernelCommandLineOptions.push_back(text(" Preempt")); + kernelCommandLineOptions.push_back(hbox(text(" "), preemptCheckBox0->Render(), text(" "))); + kernelCommandLineOptions.push_back(hbox(text(" "), preemptCheckBox1->Render(), text(" "))); + kernelCommandLineOptions.push_back(hbox(text(" "), preemptCheckBox2->Render(), text(" "))); + } + if (dynamicScheduler != DynamicSchedulerT::NotApplicable && threadedIrqsAvailable) + { + kernelCommandLineOptions.push_back(text("")); + } + if (threadedIrqsAvailable) + { + kernelCommandLineOptions.push_back(hbox(text(" "), threadedIrqCheckbox->Render(), text(" "))); + } + kernelCommandLineOptions.push_back(text("")); + } + contentElements.push_back( + window( + text(" Kernel Commandline Options "), + vbox(kernelCommandLineOptions))); + } + auto dialog = vbox( + {text(" PiPedal Kernel Configuration"), + separator(), + text(""), + hbox(text(" "), vbox(contentElements) | color(Color::White), text(" ")), + text(""), + text(""), + configRequired ? hbox(text("") | flex, cancelButton->Render(), saveButton->Render(), text(" ")) + : hbox(text("") | flex, okButton->Render(), text(" "))}) | + (borderStyled(BorderStyle::HEAVY) | color(Color::White)); + topLevelElements.push_back(std::move(center(dialog))); + } + return dbox(topLevelElements); + }); + mainRenderer = CatchEvent( + mainRenderer, + [&](Event event) + { + if (event.is_character()) + { + if (event == Event::Character('q')) + { + screen.ExitLoopClosure()(); + return true; + } + } + return false; + }); + auto processDialogRenderer = Renderer(focusTrap, [&]() + { + auto dialog = vbox( + { + text(" Applying changes..."), + hbox(text(" "),text(spinnerText(startTime)),text(" ")) + }) | borderHeavy | clear_under | center; + + return dialog; }); + + ConstStringRef rebootNowText(" Now "); + ConstStringRef rebootLaterText(" Later "); + + auto rebootNowButton = Button( + rebootNowText, + [&]() + { + dialogIndex = 0; + silentSysExec("sudo reboot now"); + quit(); + }, + ButtonOption::Ascii()); + auto rebootLaterButton = Button( + rebootLaterText, + [&]() + { + dialogIndex = 0; + quit(); + }, + ButtonOption::Ascii()); + auto rebootDialogContainer = Container::Horizontal({rebootLaterButton, rebootNowButton}); + + auto rebootDialogRenderer = Renderer( + rebootDialogContainer, + [&]() + { + return hbox({text(" "), + vbox({hbox(text(" "), paragraph("Reboot required. Would you like to reboot now or later?") | size(WIDTH, LESS_THAN, 35), text(" ")), + text(""), + hbox(text("") | flex, rebootLaterButton->Render(), rebootNowButton->Render(), text(" ")), + text(" ")})}) | + borderHeavy; + }); + + ////////////// ErrorDialog ///////////////////////////////////////////////// + + ConstStringRef errorOkText(" OK "); + + auto errorOkButton = Button( + errorOkText, + [&]() + { + dialogIndex = 0; + quit(); + }, + ButtonOption::Ascii()); + auto errorDialogContainer = Container::Horizontal({errorOkButton}); + + auto errorDialogRenderer = Renderer( + errorDialogContainer, + [&]() + { + return vbox( + {text(" Error"), + separator(), + + vbox({hbox(text(" "), paragraph(errorMessage) | size(WIDTH, LESS_THAN, 35), text(" ")), + text(""), + hbox(text("") | flex, errorOkButton->Render(), text(" ")) + + })}) | + borderHeavy; + }); + + ///////////////////////////////////////////////////////////////////////////// + + auto modalDialogContainer = Container::Tab( + { + + mainRenderer, + processDialogRenderer, + rebootDialogRenderer, + errorDialogRenderer}, + &dialogIndex); + + auto modalDialogRenderer = Renderer( + modalDialogContainer, + [&]() mutable + { + Element document = modalDialogContainer->ChildAt(0)->Render(); + if (dialogIndex != 0) + { + document = dbox( + document, + modalDialogContainer->ChildAt(dialogIndex)->Render() | clear_under | center); + } + if (dialogIndex == PROCESSING_DIALOG) + { + screen.RequestAnimationFrame(); + { + std::lock_guard lock(updateCallbackMutex); + if (updatePending && updateComplete) + { + updatePending = false; + updateComplete = false; + if (updateErrorMessage.length() != 0) + { + onError(updateErrorMessage); + updateErrorMessage = ""; + } else { + // operation complete. + // show the reboot dialog. + dialogIndex = REBOOT_DIALOG; + + } + + } + } + + } + return document; + }); + + screen.Loop(modalDialogRenderer); +} + + +void printHelp() +{ + using namespace std; + + PrettyPrinter pp; + + + pp << + "pipedal_kconfig - Edit kernel commandline arguments. \n" + << "Copyright (c) Robin E. R. Davies" + << "\n\n" + << "A utility that provides a user-friendly interface for configuring kernel commandline arguments that affect realtime audio performance on PREEMPT_DYANMIC kernels (like Ubuntu)." + << "\n\n" + << "Linux kernels that have PREEMPT_DYNAMIC kernels provide excellent support for real-time low-latency audio, but are not " + << "optimially configured for real-time audio by default. " + << "\n\n" + << "To provide best performance, the following two kernel commandline arguments need to be set:" + << "\n\n" + << Indent(20) + << HangingIndent() + << " preempt=full\tallows the kernel to preempt kernel threads` in order to service more urgent real-time audio tasks." + << "\n\n" + << HangingIndent() + << " threadirqs\tallows realtime audio tasks to run with higher priority than network and hard disk interrupt handlers." + << "\n\n" + << Indent(0) + << "These settings have no effect on PREEMT_RT kernels (like Raspberry Pi OS), since PREEMPT_RT kernels turn these features on by default." + << "\n\n" + << "pipedal_kconfig needs to run with root privileges." + << "\n\n" + ; + + +} + +int main(int argc, char**argv) +{ + bool help = false; + bool noSudo = false; + CommandLineParser cmdline; + cmdline.AddOption("-h",&help); + cmdline.AddOption("--help",&help); + cmdline.AddOption("--no-sudo",&noSudo); // undocumented option to allow debugging of most of the code, except for the finial privileged bits. + + + try { + cmdline.Parse(argc,(const char**)argv); + + if (help || cmdline.Arguments().size() != 0) + { + printHelp(); + return EXIT_SUCCESS; + } + + } catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + auto uid = getuid(); + if (uid != 0 && !noSudo) + { + return SudoExec(argc, argv); + } + + + kconfigUi(); + return EXIT_SUCCESS; +} \ No newline at end of file