diff --git a/docs/WhichLv2PluginsAreSupported.md b/docs/WhichLv2PluginsAreSupported.md index 4a5adad..dce42bc 100644 --- a/docs/WhichLv2PluginsAreSupported.md +++ b/docs/WhichLv2PluginsAreSupported.md @@ -9,9 +9,8 @@ following conditions: - Must be remotely controllable (no hard dependency on GUI-only controls), which is true of all but a tiny minority of LV2 plugins. -If you install a new LV2 plugin, PiPedal will detect the change and make it available immediately. Wait a few seconds, and the newly-installed plugin should show up in the list of available plugins. -PiPedal does not currently allow plugins to provide custom user interfaces; it displays controls that are declared by the LV2 plugin without customization. In practice, this means that LV2 plugins with simple sets of controls work well, but those with complex sets of controls can be difficult to use. However, we would be pleased to collaborate with developers who need more than the basic set of controls. This is a feature set that Pipedal needs to address, so we would be happy to collaborate with other developers on this. Please contact rerdavies at gmail.com for further details. +If you install a new LV2 plugin, PiPedal will detect the change and make it available immediately. Wait a few seconds, and the newly-installed plugin should show up in the list of available plugins. -------- [<< Using LV2 Audio Plugins](UsingLv2Plugins.md) | [Up](Documentation.md) | [MOD UI Support >>](ModUiSupport.md) diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 783ab4f..195cec6 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -35,6 +35,7 @@ #include "PiPedalException.hpp" #include "DummyAudioDriver.hpp" #include "SchedulerPriority.hpp" +#include "CrashGuard.hpp" #include "CpuUse.hpp" @@ -1595,6 +1596,8 @@ namespace pipedal throw PiPedalStateException("Unable to start ALSA capture."); } + CrashGuardLock crashGuardLock; + cpuUse.SetStartTime(cpuUse.Now()); while (true) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 787c8b5..95faca4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -206,6 +206,7 @@ else() endif() set (PIPEDAL_SOURCES + CrashGuard.cpp CrashGuard.hpp ModTemplateGenerator.cpp ModTemplateGenerator.hpp WebServerMod.cpp WebServerMod.hpp ModGui.cpp ModGui.hpp @@ -767,6 +768,7 @@ add_executable(pipedal_latency_test DummyAudioDriver.cpp DummyAudioDriver.hpp JackConfiguration.hpp JackConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp + CrashGuard.cpp CrashGuard.hpp CpuUse.hpp CpuUse.cpp ) diff --git a/src/CrashGuard.cpp b/src/CrashGuard.cpp new file mode 100644 index 0000000..472bf47 --- /dev/null +++ b/src/CrashGuard.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * 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 "CrashGuard.hpp" +#include "ofstream_synced.hpp" +#include +#include + +using namespace pipedal; +namespace fs = std::filesystem; + +int CrashGuard::crashCount = 0; +std::mutex CrashGuard::mutex; +std::filesystem::path CrashGuard::fileName; +int64_t CrashGuard::depth = 0; + +void CrashGuard::SetCrashGuardFileName(const std::filesystem::path &path) +{ + CrashGuard::fileName = path; + + { + std::ifstream f{path}; + if (f.is_open()) + { + f >> crashCount; + } + if (crashCount > 0) { + Lv2Log::info("CrashGuard: Detected previous crash, count = %d", (int)crashCount); + } + } +} +bool CrashGuard::HasCrashed() +{ + return crashCount > 4; +} +void CrashGuard::ClearCrashFlag() +{ + crashCount = 0; + try + { + if (fs::exists(fileName)) + { + fs::remove(fileName); + } + } + catch (const std::exception &ignored) + { + } +} + +void CrashGuard::EnterCrashGuardZone() +{ + std::lock_guard lock{mutex}; + + if (depth++ == 0) + { + if (!fileName.empty()) { + ofstream_synced f{fileName}; + f << (crashCount + 1) << std::endl; + } + } +} +void CrashGuard::LeaveCrashGuardZone() +{ + std::lock_guard lock{mutex}; + if (--depth == 0) + { + try + { + if (!fileName.empty() && fs::exists(fileName)) + { + fs::remove(fileName); + } + } + catch (const std::exception &ignored) + { + } + } +} diff --git a/src/CrashGuard.hpp b/src/CrashGuard.hpp new file mode 100644 index 0000000..d62839d --- /dev/null +++ b/src/CrashGuard.hpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * 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 + +namespace pipedal +{ + + /** + * @brief Crash protection. + * + * Detect the situation where PiPedal has crashed multiple times due to SIG_SEGVS caused by a plugin. + * + * The basic premise: mark segments of code as "crash protected". If Pipedal shuts down without exiting the + * crash guard zone for some fixed number of occasions (3?), then set a flag that will prevent + * loading of the default plugin when PiPedal next loads. Since PiPedal restarts indefinitely while + * running as a service, this allows users to recover after they have committed a plugin that crashes + * into their current preset. + * + */ + + class CrashGuard + { + public: + static void SetCrashGuardFileName(const std::filesystem::path &path); + static bool HasCrashed(); + static void ClearCrashFlag(); + + static void EnterCrashGuardZone(); + static void LeaveCrashGuardZone(); + + private: + static int crashCount; + static std::mutex mutex; + static std::filesystem::path fileName; + static int64_t depth; + }; + + class CrashGuardLock { + public: + CrashGuardLock() { + CrashGuard::EnterCrashGuardZone(); + } + ~CrashGuardLock() { + CrashGuard::LeaveCrashGuardZone(); + } + CrashGuardLock(const CrashGuardLock &) = delete; + CrashGuardLock &operator=(const CrashGuardLock &) = delete; + CrashGuardLock(CrashGuardLock &&) = delete; + CrashGuardLock &operator=(CrashGuardLock &&) = delete; + }; +}; \ No newline at end of file diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index 37b25bb..a03e034 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -38,6 +38,7 @@ #include #include "ss.hpp" #include "SchedulerPriority.hpp" +#include "CrashGuard.hpp" #include "CpuUse.hpp" @@ -287,6 +288,7 @@ namespace pipedal bool ok = true; + CrashGuardLock crashGuardLock; while (true) { diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 5fa82e0..df6d0bd 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -27,6 +27,7 @@ #include "AudioHost.hpp" #include "Lv2EventBufferWriter.hpp" #include "Lv2Log.hpp" +#include "CrashGuard.hpp" using namespace pipedal; @@ -401,6 +402,8 @@ void Lv2Pedalboard::UpdateAudioPorts() void Lv2Pedalboard::Activate() { + CrashGuardLock crashGuardLock; + for (int i = 0; i < this->effects.size(); ++i) { this->realtimeEffects[i]->Activate(); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index d79b286..a3b4e95 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -45,6 +45,7 @@ #include "AvahiService.hpp" #include "DummyAudioDriver.hpp" #include "AudioFiles.hpp" +#include "CrashGuard.hpp" #ifndef NO_MLOCK #include @@ -271,20 +272,33 @@ void PiPedalModel::Load() this->pedalboard = storage.GetCurrentPreset(); // the current *saved* preset. + + // the current edited preset, saved only across orderly shutdowns. - CurrentPreset currentPreset; - try - { - if (storage.RestoreCurrentPreset(¤tPreset)) + + CrashGuard::SetCrashGuardFileName(storage.GetDataRoot() / "crash_guard.data"); + + if (CrashGuard::HasCrashed()) { + // ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash. + this->pedalboard = Pedalboard::MakeDefault(); + } else { + CurrentPreset currentPreset; + try { - this->pedalboard = currentPreset.preset_; - this->hasPresetChanged = currentPreset.modified_; + if (storage.RestoreCurrentPreset(¤tPreset)) + { + this->pedalboard = currentPreset.preset_; + this->hasPresetChanged = currentPreset.modified_; + } + } + catch (const std::exception &e) + { + Lv2Log::warning(SS("Failed to load current preset. " << e.what())); } } - catch (const std::exception &e) - { - Lv2Log::warning(SS("Failed to load current preset. " << e.what())); - } + + + UpdateDefaults(&this->pedalboard); std::unique_ptr p{AudioHost::CreateInstance(pluginHost.asIHost())}; @@ -2610,6 +2624,7 @@ void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedal bool PiPedalModel::LoadCurrentPedalboard() { + CrashGuardLock crashGuardLock; if (previousPedalboardLoaded && pedalboard.IsStructureIdentical(previousPedalboard)) { // then we can send a snapshot update instead!