diff --git a/.vscode/launch.json b/.vscode/launch.json index d459cd3..cc7a005 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -112,7 +112,7 @@ //"[json_variants]" // subtest of your choice, or none to run all of the tests. //"[inverting_mutex_test]" // "[utf8_to_utf32]" - "[wifi_channels_test]" + "[updater]" ], "stopAtEntry": false, @@ -258,10 +258,11 @@ "program": "${command:cmake.launchTargetPath}", "args": [ "--nosudo", // run without sudo (which will fail because of perms, but allow us to debug deeper into install code.) + "--prefix", "/usr", //"--get-current-port" - // "--uninstall" + "--install" //"--enable-p2p" , "CA", "PiPedalTest","12345678","14" - "--list-p2p-channels" + //"--list-p2p-channels" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8796e44..dbcb7cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,19 +1,16 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.2.41 + VERSION 1.2.44 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "v1.2.41") - +set (DISPLAY_VERSION "PiPedal v1.2.42-Experimental") +set (PACKAGE_ARCHITECTURE "arm64") set (CMAKE_INSTALL_PREFIX "/usr/") include(CTest) enable_testing() -# Frameworks -# catch - add_subdirectory("submodules/pipedal_p2pd") add_subdirectory("PiPedalCommon") @@ -95,7 +92,7 @@ set(CPACK_DEBIAN_PACKAGE_SECTION sound) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) #set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dhcpcd, dnsmasq" ) -set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, dhcpcd,dnsmasq, authbind" ) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, dhcpcd,dnsmasq, authbind, gpg" ) #set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) @@ -103,6 +100,8 @@ set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) set (CPACK_STRIP_FILES ON) +set(CPACK_DEBIAN_PACKAGE_SIGN_ALGORITHM "detached") +set(CPACK_DEBIAN_PACKAGE_SIGN_TYPE "origin") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/debian/prerm") diff --git a/NetworkManagerP2P/src/NMP2pSettings.cpp b/NetworkManagerP2P/src/NMP2pSettings.cpp index 598c733..21b9917 100644 --- a/NetworkManagerP2P/src/NMP2pSettings.cpp +++ b/NetworkManagerP2P/src/NMP2pSettings.cpp @@ -9,14 +9,17 @@ #include "WifiRegs.hpp" #include "ChannelInfo.hpp" #include "DBusLog.hpp" +#include using namespace pipedal; -P2pSettings::P2pSettings(const std::filesystem::path&configDirectoryPath) +P2pSettings::P2pSettings(const std::filesystem::path&configDirectoryPath, const std::filesystem::path&varDirectoryPath) +: configDirectoryPath(configDirectoryPath), + varDirectoryPath(varDirectoryPath) + { - this->configDirectoryPath = configDirectoryPath; } - + static const char*hexDigits = "0123456789abcdef"; std::string MakeBssid() { @@ -188,12 +191,40 @@ void P2pSettings::Load() } } + +static void openWithPerms( + std::ofstream &f, + const std::filesystem::path &path, + std::filesystem::perms perms = + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | std::filesystem::perms::group_write) +{ + auto directory = path.parent_path(); + std::filesystem::create_directories(directory); + // open and close to make an existing empty file. + // close it. + { + std::ofstream f; + f.open(path); + f.close(); + } + // set the perms. + std::filesystem::permissions( + path, + perms, + std::filesystem::perm_options::replace); + + // open for re3al. + f.open(path); +} void P2pSettings::Save() { auto filename = config_filename(); try { - std::ofstream f(filename); - if (!f) + std::ofstream f; + openWithPerms(f,filename); + + if (!f.is_open()) { throw std::runtime_error("Can't write to file."); } diff --git a/NetworkManagerP2P/src/NMP2pSettings.hpp b/NetworkManagerP2P/src/NMP2pSettings.hpp index 6e8d11a..650cbeb 100644 --- a/NetworkManagerP2P/src/NMP2pSettings.hpp +++ b/NetworkManagerP2P/src/NMP2pSettings.hpp @@ -8,7 +8,9 @@ class P2pSettings { // adapter between nm p2p settings, and legacy p2p public: - P2pSettings(const std::filesystem::path&configDirectory = "/etc/pipedal/config"); + P2pSettings( + const std::filesystem::path&configDirectory = "/etc/pipedal/config", + const std::filesystem::path&varDirectory = "/var/pipedal/config" ); void Load(); void Save(); @@ -16,6 +18,7 @@ public: private: bool valid_ = false; std::filesystem::path configDirectoryPath; + std::filesystem::path varDirectoryPath; bool auth_changed_ = true; std::vector device_type_{ 0x00,0x01, 0x00,0x50,0xF2,0x04, 0x00,0x02 /*"1-0050F204-2";*/ @@ -59,7 +62,7 @@ public: }; std::filesystem::path config_filename() const { // "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"; } - return (configDirectoryPath / "NetworkManagerP2P.json").string(); + return (varDirectoryPath / "NetworkManagerP2P.json").string(); }; const std::string& bssid() const { return bssid_; } diff --git a/PiPedalCommon/src/ServiceConfiguration.cpp b/PiPedalCommon/src/ServiceConfiguration.cpp index ae8abb7..7afb2ce 100644 --- a/PiPedalCommon/src/ServiceConfiguration.cpp +++ b/PiPedalCommon/src/ServiceConfiguration.cpp @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2022 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 @@ -23,6 +23,7 @@ */ #include "ServiceConfiguration.hpp" +#include #include #include #include @@ -35,43 +36,49 @@ using namespace pipedal; using namespace std; using namespace config_serializer; - -const char ServiceConfiguration::DEVICEID_FILE_NAME[] = "/etc/pipedal/config/service.conf"; +const char ServiceConfiguration::DEVICEID_FILE_NAME[] = "/var/pipedal/config/service.conf"; #define SERIALIZER_ENTRY(MEMBER_NAME) \ - new ConfigSerializer(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "") - + new ConfigSerializer(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "") static p2p::autoptr_vector> -deviceIdSerializers -{ - SERIALIZER_ENTRY(uuid), - SERIALIZER_ENTRY(deviceName), - SERIALIZER_ENTRY(server_port) -}; - + deviceIdSerializers{ + SERIALIZER_ENTRY(uuid), + SERIALIZER_ENTRY(deviceName), + SERIALIZER_ENTRY(server_port)}; ServiceConfiguration::ServiceConfiguration() -: base(deviceIdSerializers) + : base(deviceIdSerializers) { - + filename = "/var/pipedal/config/service.conf"; } -void ServiceConfiguration::Load() +void ServiceConfiguration::Load(std::filesystem::path filename) { - base::Load(DEVICEID_FILE_NAME,false); + this->filename = filename; + base::Load(filename, false); } void ServiceConfiguration::Save() { - base::Save(DEVICEID_FILE_NAME); { - struct group *group_; + auto directory = filename.parent_path(); + std::filesystem::create_directories(directory); + + // make sure the file has correct permissions. + std::ofstream t; + t.open(filename); + t.close(); + + struct group *group_; group_ = getgrnam("pipedal_d"); if (group_ == nullptr) { throw logic_error("Group not found: pipedal_d"); } - std::ignore = chown(DEVICEID_FILE_NAME,-1,group_->gr_gid); - std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); + std::ignore = chown(DEVICEID_FILE_NAME, -1, group_->gr_gid); + std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH); + } + + base::Save(filename); } \ No newline at end of file diff --git a/PiPedalCommon/src/SysExec.cpp b/PiPedalCommon/src/SysExec.cpp index e97cb41..8506b34 100644 --- a/PiPedalCommon/src/SysExec.cpp +++ b/PiPedalCommon/src/SysExec.cpp @@ -267,3 +267,42 @@ int pipedal::sysExecWait(ProcessId pid_) return exitStatus; } + +SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::string &args) +{ + namespace fs = std::filesystem; + fs::path fullPath = findOnSystemPath(program); + if (!fs::exists(fullPath)) + { + throw std::runtime_error(SS("Path does not exist. " << fullPath)); + } + std::stringstream s; + s << fullPath.c_str() << " " << args << " 2>&1"; + + std::string fullCommand = s.str(); + FILE *output = popen(fullCommand.c_str(), "r"); + if (output) + { + char buffer[512]; + std::stringstream ssOutput; + + while (!feof(output)) + { + size_t nRead = fread(buffer, sizeof(char),sizeof(buffer), output); + ssOutput.write(buffer,nRead); + if (nRead == 0) + break; + + } + int rc = pclose(output); + SysExecOutput result + { + .exitCode = rc, + .output = ssOutput.str() + }; + return result; + } else { + throw std::runtime_error(SS("Failed to execute command. " << fullCommand )); + } + +} diff --git a/PiPedalCommon/src/WifiDirectConfigSettings.cpp b/PiPedalCommon/src/WifiDirectConfigSettings.cpp index eb9c9ac..049a8ee 100644 --- a/PiPedalCommon/src/WifiDirectConfigSettings.cpp +++ b/PiPedalCommon/src/WifiDirectConfigSettings.cpp @@ -158,12 +158,12 @@ void WifiDirectConfigSettings::Load() { this->enable_ = false; std::string strEnable; - if (ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","enabled",&strEnable)) + if (ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","enabled",&strEnable)) { this->enable_ = (strEnable == "true" || strEnable == "1"); } std::string strWlan; - if (ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","wlan",&strWlan)) + if (ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","wlan",&strWlan)) { this->wlan_ = strWlan; } else { @@ -172,20 +172,20 @@ void WifiDirectConfigSettings::Load() - if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","p2p_device_name",&this->hotspotName_)) + if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","p2p_device_name",&this->hotspotName_)) { this->hotspotName_ = "PiPedal"; } - if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","country_code",&this->countryCode_)) + if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","country_code",&this->countryCode_)) { this->countryCode_ = "US"; } - if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","p2p_pin",&this->pin_)) + if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","p2p_pin",&this->pin_)) { this->pin_ = "12345678"; } - if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","wifiChannel",&this->channel_)) + if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","wifiChannel",&this->channel_)) { this->channel_ = "6"; } diff --git a/PiPedalCommon/src/include/ServiceConfiguration.hpp b/PiPedalCommon/src/include/ServiceConfiguration.hpp index e437ec2..6749454 100644 --- a/PiPedalCommon/src/include/ServiceConfiguration.hpp +++ b/PiPedalCommon/src/include/ServiceConfiguration.hpp @@ -26,6 +26,7 @@ #include #include "ConfigSerializer.hpp" +#include namespace pipedal { using namespace config_serializer; @@ -38,12 +39,16 @@ namespace pipedal { static const char DEVICEID_FILE_NAME[]; - void Load(); + void Load( + std::filesystem::path path = "/var/pipedal/config/service.conf" + ); void Save(); std::string uuid; std::string deviceName = "PiPedal"; uint32_t server_port = 80; + private: + std::filesystem::path filename; }; } \ No newline at end of file diff --git a/PiPedalCommon/src/include/SysExec.hpp b/PiPedalCommon/src/include/SysExec.hpp index 1bacde5..031b90d 100644 --- a/PiPedalCommon/src/include/SysExec.hpp +++ b/PiPedalCommon/src/include/SysExec.hpp @@ -20,20 +20,28 @@ #pragma once #include -namespace pipedal { -// exec a command, returning the actual exit code (unlike execXX() or system() ) -int sysExec(const char*szCommand); +namespace pipedal +{ + // exec a command, returning the actual exit code (unlike execXX() or system() ) + int sysExec(const char *szCommand); -// execute a command, suppressing output. -int silentSysExec(const char *szCommand); + // execute a command, suppressing output. + int silentSysExec(const char *szCommand); + struct SysExecOutput + { + int exitCode; + std::string output; + }; -using ProcessId = int64_t; // platform-agnostic wrapper for pid_t; -// Returns a pid or -1 on errror. -ProcessId sysExecAsync(const std::string&command); + SysExecOutput sysExecForOutput(const std::string& command, const std::string&args); -void sysExecTerminate(ProcessId pid_,int termTimeoutMs = 1000,int killTimeoutMs = 500 ); // returns the process's exit status. -int sysExecWait(ProcessId pid); + using ProcessId = int64_t; // platform-agnostic wrapper for pid_t; + // Returns a pid or -1 on errror. + ProcessId sysExecAsync(const std::string &command); -std::string getSelfExePath(); + void sysExecTerminate(ProcessId pid_, int termTimeoutMs = 1000, int killTimeoutMs = 500); // returns the process's exit status. + int sysExecWait(ProcessId pid); + + std::string getSelfExePath(); } \ No newline at end of file diff --git a/PiPedalCommon/src/include/json_variant.hpp b/PiPedalCommon/src/include/json_variant.hpp index 88d43e0..c972ace 100644 --- a/PiPedalCommon/src/include/json_variant.hpp +++ b/PiPedalCommon/src/include/json_variant.hpp @@ -68,6 +68,7 @@ namespace pipedal public: ~json_variant(); json_variant(); + json_variant(json_reader &reader); json_variant(json_variant &&); json_variant(const json_variant &); @@ -81,9 +82,9 @@ namespace pipedal json_variant(const std::shared_ptr &value); json_variant(json_array &&array); json_variant(json_object &&object); - json_variant(const char*sz); + json_variant(const char *sz); - json_variant(const void*) = delete; // do NOT allow implicit conversion of pointers to bool + json_variant(const void *) = delete; // do NOT allow implicit conversion of pointers to bool json_variant &operator=(json_variant &&value); json_variant &operator=(const json_variant &value); @@ -94,9 +95,9 @@ namespace pipedal json_variant &operator=(json_object &&value); json_variant &operator=(json_array &&value); - json_variant &operator=(const char*sz) { return (*this) = std::string(sz); } + json_variant &operator=(const char *sz) { return (*this) = std::string(sz); } - json_variant &operator=(void*) = delete; // do NOT allow implicit conversion of pointers to bool + json_variant &operator=(void *) = delete; // do NOT allow implicit conversion of pointers to bool void require_type(ContentType content_type) const { @@ -145,6 +146,22 @@ namespace pipedal return content.double_value; } + int8_t as_int8() const { return (int8_t)as_number(); } + + uint8_t as_uint8() const { return (uint8_t)as_number(); } + + int16_t as_int16() const { return (int16_t)as_number(); } + + uint16_t as_uint16() const { return (uint16_t)as_number(); } + + int32_t as_int32() const { return (int32_t)as_number(); } + + uint32_t as_uint32() const { return (uint32_t)as_number(); } + + int64_t as_int64() const { return (int64_t)as_number(); } + + uint64_t as_uint64() const { return (uint64_t)as_number(); } + const std::string &as_string() const; std::string &as_string(); @@ -154,9 +171,6 @@ namespace pipedal const std::shared_ptr &as_array() const; std::shared_ptr &as_array(); - template - U &as() { static_assert("Invalid type."); } - // convenience methods for object and array manipulation. static json_variant make_object(); static json_variant make_array(); @@ -179,6 +193,7 @@ namespace pipedal bool operator!=(const json_variant &other) const; std::string to_string() const; + private: void free(); void write_double_value(json_writer &writer, double value) const; @@ -212,12 +227,12 @@ namespace pipedal class json_array : public JsonSerializable { private: - json_array(const json_array&) { } // deleted. + json_array(const json_array &) {} // deleted. public: using ptr = std::shared_ptr; json_array() { ++allocation_count_; } - json_array(json_array&&other); + json_array(json_array &&other); ~json_array() { --allocation_count_; } json_variant &at(size_t index); @@ -247,7 +262,7 @@ namespace pipedal } using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; - + iterator begin() { return values.begin(); } iterator end() { return values.end(); } const_iterator begin() const { return values.begin(); } @@ -266,13 +281,13 @@ namespace pipedal class json_object : public JsonSerializable { private: - json_object(const json_object&) { } // deleted. + json_object(const json_object &) {} // deleted. public: using ptr = std::shared_ptr; - json_object() { ++ allocation_count_;} - json_object(json_object&&other); - ~json_object() { --allocation_count_;} + json_object() { ++allocation_count_; } + json_object(json_object &&other); + ~json_object() { --allocation_count_; } size_t size() const { return values.size(); } json_variant &at(const std::string &index); @@ -285,25 +300,24 @@ namespace pipedal bool operator!=(const json_object &other) const { return (!((*this) == other)); } bool contains(const std::string &index) const; - - using values_t = std::vector< std::pair >; + using values_t = std::vector>; using iterator = values_t::iterator; using const_iterator = values_t::const_iterator; - + iterator begin() { return values.begin(); } iterator end() { return values.end(); } const_iterator begin() const { return values.begin(); } const_iterator end() const { return values.end(); } - iterator find(const std::string& key); - const_iterator find(const std::string& key) const; - + iterator find(const std::string &key); + const_iterator find(const std::string &key) const; // strictly for testing purposes. Not thread-safe. - static int64_t allocation_count() + static int64_t allocation_count() { return allocation_count_; } + private: virtual void read_json(json_reader &reader); virtual void write_json(json_writer &writer) const; @@ -317,27 +331,6 @@ namespace pipedal inline std::string &json_variant::memString() { return *(std::string *)content.mem; } inline const std::string &json_variant::memString() const { return *(const std::string *)content.mem; } - template <> - inline json_null &json_variant::as() { return as_null(); } - - template <> - inline bool &json_variant::as() { return as_bool(); } - - template <> - inline double &json_variant::as() { return as_number(); } - - template <> - inline std::string &json_variant::as() { return as_string(); } - - template <> - inline std::shared_ptr &json_variant::as>() { return as_object(); } - - template <> - inline std::shared_ptr &json_variant::as>() { return as_array(); } - - template <> - inline json_variant &json_variant::as() { return *this; } - inline json_variant::object_ptr &json_variant::memObject() { return *(object_ptr *)content.mem; @@ -525,22 +518,23 @@ namespace pipedal return !(*this == other); } - // Holds a string but is json_read and json_written as an unqoted json object. - class raw_json_string: public JsonSerializable { - public: - raw_json_string() { } - raw_json_string(const std::string &value) : value(value) {} - const std::string& as_string() const { return value; } + class raw_json_string : public JsonSerializable + { + public: + raw_json_string() {} + raw_json_string(const std::string &value) : value(value) {} + const std::string &as_string() const { return value; } - void Set(const std::string&value) { this->value = value; } + void Set(const std::string &value) { this->value = value; } - private: - - virtual void write_json(json_writer &writer) const { + private: + virtual void write_json(json_writer &writer) const + { writer.write_raw(value.c_str()); } - virtual void read_json(json_reader &reader) { + virtual void read_json(json_reader &reader) + { throw std::logic_error("Not implemented."); } diff --git a/PiPedalCommon/src/json_variant.cpp b/PiPedalCommon/src/json_variant.cpp index 8c1b3ae..aa1b385 100644 --- a/PiPedalCommon/src/json_variant.cpp +++ b/PiPedalCommon/src/json_variant.cpp @@ -552,6 +552,11 @@ json_variant::json_variant(const char*sz) } +json_variant::json_variant(json_reader&reader) +{ + this->read_json(reader); +} + /*static*/ json_null json_null::instance; diff --git a/README.md b/README.md index 736934a..e68cbae 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ -Download: v1.2.41 +Download: v1.2.42 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).   -#### NEW version 1.2.41 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.2.44 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   diff --git a/config/config.json b/config/config.json index 4541f52..d87a2c3 100644 --- a/config/config.json +++ b/config/config.json @@ -1,33 +1,39 @@ { + + // DO NOT EDIT THIS FILE + // + // THIS FILE CONTAINS DEFAULT VALUES AS SET BY THE INSTALLER, AND IS OVERWRITTEN BY + // UPGRADES. + // + // If you wish to customize any of these values, see /var/pipedal/config/config.json + + /* A writable directory in which state files can be stored, preferrably only by the server process. */ + "local_storage_path": "/var/pipedal", + /* One or more directories containing LV2 plugins, seperated by ':' */ "lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2", - /* A writable directory in which state files can be stored, preferrably only by the server process. */ - "local_storage_path": "/var/pipedal", - /* whether to lock a process pages into memroy. should be true unless running on a very memory constrained system. - Setting to false will cause unpredictable audio dropouts. + /* whether to lock a process pages into memor. should be true unless running on a very memory constrained system. + Setting to false may cause unpredictable audio dropouts. */ "mlock": true, - /* Address on which Piddle listens for websocket connections */ - "socketServerAddress": "0.0.0.0:80", - /* Number of threads to use for servicing websockets */ "threads" : 5, + + /* Address on which the web server listens for http requests. */ + "socketServerAddress": "0.0.0.0:80", + + /* Whether to log individual http requests. (inadvisable) */ "logHttpRequests": false, + /* Log level for the web server */ /* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */ "logLevel": 3, /* Maximum filesize to allow when uploading */ - "maxUploadSize": 536870912, - - /* Provide access point capture redirects on this gateway. -- provides automatic browser launching on Access Point access. - (not implemented) - */ - "accessPointGateway": "172.24.1.0/24", - "accessPointServerAddress": "172.24.1.1" + "maxUploadSize": 536870912 // 512MiB } \ No newline at end of file diff --git a/config/updatekey.gpg b/config/updatekey.gpg new file mode 100644 index 0000000..30b843f --- /dev/null +++ b/config/updatekey.gpg @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGbOVk8BDAD31asK3tIVqn66DiedrambXgA+DIua4aaruI/dyUiTr4vtF2r3 +aAeZwkG3Wk01DYIEUXZpMG2CKUKV8hJlfIaR5SZxigHFo/tp4kGYk1UjIN12C8XO +qyDLsgEftsOimU6UZpxVaMN6GrhR8i2efygMQpC/6wsDJfwsqrUyM/Z/SyQeW/Cj +LSxUn+wkmsDq0HmITtoa2kKLSAVSyngmvDwIdYANdBBvq97Xaw27mDWt47K/qLwv +tT2b+yCRTTqMD/IWTCX1VsAuiglBJBqbdH/w9tdeTJzXNmmPcI5jFk/ad0YGvgo3 +8xjmN/YaueIiegJpIiOOv3X5/hlRNjKWITaiPTbzkVTfkggw9QBl0liolia7EfLR +TVxTa2j+F0G0+oWtTe8CiyejIgOqJjmEVARUjuerG+Ezy6HEWcz0e0vM+lkYzEVq +IvgPDzQ3lilYUSRvxFw95D1xZmTo87nC++Kw7NGC+Jd9GANHgrxJWhDo+l/DOK/E +/MpWhwwfUnyTBwUAEQEAAbQiUm9iaW4gRGF2aWVzIDxyZXJkYXZpZXNAZ21haWwu +Y29tPokBzgQTAQoAOBYhBDgRJOK7RHjSJdIxOyrvP3vVPqpZBQJmzlZPAhsDBQsJ +CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJECrvP3vVPqpZxkcMAIN+XQqhkhIQpOIR +RW38pYGvf9kG6eQfWDzZYnnatBUdsO2soipzqtOjyRutESG1vayJ2fuSowaoI+kV +kVpjfS7rVE31uCM9pOYzjwZfTpj82AXku7aqLRg3cgQMnZuvrjbQ0WyjA+h2Nu0h +IBvBUUxFMNuaDVsGq5kx4D+ncWdiDuc7H8pR6pAiltnpUYEXJuU+lrBeMYKnz8f4 +htj73fl7fSlkeecssHz3rC6txil3bfgzNN9bIS+MfGteRE4JmBd0c4eyArQuqHKm +INgIz7bySQwPBVmJO/26f/ANMoMJc3c8ukGTR5pzNESxYlzugUATf6H7KYXBOkGG +RgsFqoa9H/i/eHUWIZVAhXi9FIUgdqQhyKzkM/bG18muBieZpKjTquTvnf1Wj88L +4GJzT01vwO/gWAYawf4FEMGyfadZ7KT0L5O+8BcLrhhKvg/VuWGSgtRcgJZDeWdd +mxUpHZAHb2/0YqVprXL18TUD8dfCIXSa6BlRtPogZlxduzrCzrkBjQRmzlZPAQwA +vfBhU09D3WrakekpbMmCzOcKFQ+CGJ0OxisHWgkb9iLJ+bae3HIvy6WQ/cmIM6BG +/e9/mhOFpMbZ+xDl7lpuT4qZZwTtDGWLIlxYBRu9b3kKWEP2+/F6GZDjjJVVidEe +bko7qYPUgSXwX3XkwT8T5HiTFesY0sPAo0fEcTEyVjg2thcnuCtlj7oOxQO/+k9O +Dmc4rdGgghMXTX6zWHaTWk0CGHInaFQ08qmiFz7/DDW3SkrdK6l1BMg+ReGhybM+ +lzUMmlc5cFj8OyMZpVF7g8Vy2/4V8i4PneCwzVmHT1ShdNvdLUg6U6vG1/eHL5uS +32FfprdwlfALDv9MIdS7uTTfTThgODCSf5BwC4S+ZLnKlstRMH4Z1NzH/VRtRj8R +rpGgOLsYcy3Q3CTuinKGxe/BdyRgvmSgtdcrJEKClyMwHtBZIWaE2/t2lipeWllZ +NOz9+3rMy003GqzrlV+bIRBFp4JA9+mtROQn4frY/kMiaUo+Q12VxxyL7XeoeeUR +ABEBAAGJAbYEGAEKACAWIQQ4ESTiu0R40iXSMTsq7z971T6qWQUCZs5WTwIbDAAK +CRAq7z971T6qWXk/DAC6vryv/mxICrOCmJTL19+AO7J/94Q9Yo0TEVLDr5xEGgpP +FulrHKMEa8k09yK7TJaFRCIfdu4PGztLn28XL7psY+NjMwRlqCMSY5ppjJKujVzR +kYI2YSStfnO7y8bdzPNKEXhyFO5j9gaR/Xi7c1UXnLULOY07d8TRE66SNhyJVBTH +fQMoCldFjd/Um7lYNjEY9REO0m+ZWSaWIUYI5LXDhVdPh8wIsFgW33kDy12w/vZE +DauYStxFHflpdQec8DB9bmFLyG5Ov9g8x+BzJyg3fELb+9sBPcEA787+XDT8PMO2 +fie7Qxpq3DAJW9KK236WPHP3oWprAAzcyumwPq5m47sVSCFolhbg3r4iAzwdCsuv +TSH2rpsGfBWJNs4qZSuYbtv1ZesE44+Qgh1Y/iAEJLqAT23QcIe1ZowOJB3bbXIV +JrqWVJ6BzDOy/yllbSFmrM44LASP5C6kGwddx3R4RDLccYKs/NkuucZJc+sL3k0a +QOsNQMsqIgaFJgS/9Yc= +=d5rj +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/Installing.md b/docs/Installing.md index 7b3f0fd..13f6cea 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,17 +13,17 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (64-bit) v1.2.41](https://github.com/rerdavies/pipedal/releases/download/) +- [Raspberry Pi OS bookworm (64-bit) v1.2.42](https://github.com/rerdavies/pipedal/releases/download/) - [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb) -Version 1.2.41 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. +Version 1.2.44 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install pipedal_1.2.41_arm64.deb + sudo apt-get install pipedal_1.2.42_arm64.deb ``` Adjust accordingly if you have downloaded v1.1.31. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 51d7072..21f545f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,4 +1,24 @@ # Release Notes + +## PiPedal 1.2.44 Release + +This version includes the following new features: + +- Double-tap (or double-click) dial controls to reset them to default values. +- Update PiPedal to the latest version from within the PiPedal web UI or PiPedal client. PiPedal monitors the PiPedal github repository for new releases, + and prompts you to update when new releases become available. +- Choose whether to monitor Release, Beta or Developer update streams, or disable update monitoring altogether. +- All PiPedal configuration settings now preserved when upgrading. +- Advanced configuration options are now stored in /var/pipedal/config/config.json so that they are preserved when upgrading. + + + +Bug fixes: + +- Fixed a bug which prevents the PiPedal UI from updating properly when using TooB Convolution Reverb and CabIR effects. +- Plugin controls now work when there's no audio device running, or when the audio device stops. +- Minor style and theming issues. + ## PiPedal 1.2.41 Release This version includes the following new features: diff --git a/docs/download.md b/docs/download.md index d82a39a..46dc457 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,17 +4,17 @@ Download the most recent Debian (.deb) package for your platform: -- Raspberry Pi OS Bookworm (64-bit) v1.2.41 +- Raspberry Pi OS Bookworm (64-bit) v1.2.42 - Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31 -v1.2.41 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS. +v1.2.42 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS. Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.2.41_arm64.deb + sudo apt-get install ./pipedal_1.2.42_arm64.deb ``` Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. diff --git a/docs/img/Updates-sshot.png b/docs/img/Updates-sshot.png new file mode 100644 index 0000000..e5a36b7 Binary files /dev/null and b/docs/img/Updates-sshot.png differ diff --git a/docs/index.md b/docs/index.md index 7a77c70..33f3fd1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v1.2.41 +v1.2.42   @@ -9,7 +9,7 @@ To download PiPedal, click [here](download.md). To view PiPedal documentation, click [here](Documentation.md). -#### NEW version 1.2.41 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.2.44 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   diff --git a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl index 3353fa9..a93abac 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl @@ -88,7 +88,7 @@ cabir:impulseFile3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 89ce418..7aa0b69 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -49,7 +49,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; mod:brand "TooB"; mod:label "TooB CabSim"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl index 2e5a2d9..a176feb 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -51,7 +51,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl index 855bb7b..c2b131d 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl @@ -49,7 +49,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 1531a55..3bc38f7 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -65,7 +65,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; mod:brand "TooB"; mod:label "TooB Input"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 0586574..afe8bf0 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -58,7 +58,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index de2c61e..bfb4ae9 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -55,7 +55,7 @@ tonestack:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 index e62ad8b..19a4155 120000 --- a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 +++ b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 @@ -1 +1 @@ -ToobAmp.so.1.1.36 \ No newline at end of file +ToobAmp.so.1.1.43 \ No newline at end of file diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.30 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.30 deleted file mode 100644 index 83370f1..0000000 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.30 and /dev/null differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.31 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.31 deleted file mode 100644 index 28bc024..0000000 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.31 and /dev/null differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.36 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.43 similarity index 65% rename from lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.36 rename to lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.43 index 7bd073c..e462d69 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.36 and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.43 differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index febb08e..5972fe4 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index ddeeff9..9748da0 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ Emulation of a Boss CE-2 Chorus. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index c901bfe..4dcb031 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ A straightforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl index 429851a..dce7c25 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl index 9f7eeb1..a2b4ef5 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ Digital emulation of a Boss BF-2 Flanger. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index 6ae6c9f..bc1f13b 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index c47f31f..6d02c04 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -63,7 +63,7 @@ toobml:sagGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using neural-network-based machine learning models of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl index 0141fa6..ff55aab 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl @@ -58,7 +58,7 @@ toobNam:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ A port of Steven Atkinson's Neural Amp Modeler to LV2. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index 2296242..fa38846 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 36 ; + lv2:microVersion 43 ; rdfs:comment """ TooB Tuner is a chromatic guitar tuner. """ ; diff --git a/react/CMakeLists.txt b/react/CMakeLists.txt index 1f2d20b..862f75d 100644 --- a/react/CMakeLists.txt +++ b/react/CMakeLists.txt @@ -21,6 +21,7 @@ add_custom_command( WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react DEPENDS + src/Updater.tsx src/OkCancelDialog.tsx src/PluginControl.tsx src/svg/ic_save_bank_as.svg diff --git a/react/public/var/config.json b/react/public/var/config.json index 04d3a08..d16ec84 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 80, + "socket_server_port": 81, "socket_server_address": "*", "debug": true, "max_upload_size": 536870912, diff --git a/react/src/AboutDialog.tsx b/react/src/AboutDialog.tsx index ab7716f..395e06b 100644 --- a/react/src/AboutDialog.tsx +++ b/react/src/AboutDialog.tsx @@ -156,7 +156,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( }) .catch((err) => { // ok in debug builds. File doesn't get placed until install time. - console.log("Failed to fetch open-source notices. " + err); + console.log("Failed to fetch open-source notices. " + err.toString()); }); } } @@ -184,7 +184,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( render() { let classes = this.props.classes; - + let addressKey = 0; return ( { this.props.onClose() }} TransitionComponent={Transition} @@ -228,7 +228,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( {this.model.isAndroidHosted() && ( - Copyright © 2022 Robin Davies. + Copyright © 2022-2024 Robin Davies. )} @@ -239,7 +239,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( { this.model.serverVersion?.webAddresses.map((address) => ( - + {address} )) diff --git a/react/src/AndroidHost.tsx b/react/src/AndroidHost.tsx index 6c853b9..bd93f1a 100644 --- a/react/src/AndroidHost.tsx +++ b/react/src/AndroidHost.tsx @@ -28,7 +28,7 @@ export interface AndroidHostInterface { getHostVersion() : string; chooseNewDevice() : void; setDisconnected(isDisconnected: boolean): void; - + launchExternalUrl(url:string): boolean; }; export class FakeAndroidHost implements AndroidHostInterface @@ -47,4 +47,8 @@ export class FakeAndroidHost implements AndroidHostInterface } showSponsorship() : void { } -} \ No newline at end of file + launchExternalUrl(url:string): boolean + { + return false; + } +} diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index 56829fc..fc526dc 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog'; import AboutDialog from './AboutDialog'; import BankDialog from './BankDialog'; -import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel'; +import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel'; import ZoomedUiControl from './ZoomedUiControl' import MainPage from './MainPage'; import DialogContent from '@mui/material/DialogContent'; @@ -60,6 +60,7 @@ import RenameDialog from './RenameDialog'; import JackStatusView from './JackStatusView'; import { Theme } from '@mui/material/styles'; import { isDarkMode } from './DarkMode'; +import UpdateDialog from './UpdateDialog'; import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg'; import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg'; @@ -270,6 +271,7 @@ type AppState = { tinyToolBar: boolean; alertDialogOpen: boolean; alertDialogMessage: string; + updateDialogOpen: boolean; isSettingsDialogOpen: boolean; onboarding: boolean; isDebug: boolean; @@ -324,6 +326,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< alertDialogMessage: "", presetName: this.model_.presets.get().getSelectedText(), isSettingsDialogOpen: false, + updateDialogOpen: false, onboarding: false, isDebug: true, presetChanged: this.model_.presets.get().presetChanged, @@ -339,8 +342,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< }; + this.promptForUpdateHandler = this.promptForUpdateHandler.bind(this); this.errorChangeHandler_ = this.setErrorMessage.bind(this); - this.unmountListener = this.unmountListener.bind(this); + this.beforeUnloadListener = this.beforeUnloadListener.bind(this); + this.unloadListener = this.unloadListener.bind(this); this.stateChangeHandler_ = this.setDisplayState.bind(this); this.presetChangedHandler = this.presetChangedHandler.bind(this); this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this); @@ -353,7 +358,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< onOpenBank(bankId: number) { this.model_.openBank(bankId) - .catch((error) => this.model_.showAlert(error)); + .catch((error) => this.model_.showAlert(error.toString())); } @@ -378,7 +383,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< }); this.model_.saveBankAs(this.model_.banks.get().selectedBank, newName) .catch((error) => { - this.model_.showAlert(error); + this.model_.showAlert(error.toString()); }); } @@ -402,7 +407,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< }); this.model_.renameBank(this.model_.banks.get().selectedBank, newName) .catch((error) => { - this.model_.showAlert(error); + this.model_.showAlert(error.toString()); }); } @@ -510,18 +515,26 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.setState({ isFullScreen: !this.state.isFullScreen }); } - private unmountListener(e: Event) { - if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) { - e.preventDefault(); - (e as any).returnValue = "Are you sure you want to leave this page?"; - return "Are you sure you want to leave this page?"; - } + private unloadListener(e: Event) { + this.model_.close(); return undefined; } + + private beforeUnloadListener(e: Event) { + this.model_.close(); + return undefined; + } + promptForUpdateHandler(newValue: boolean) { + if (this.state.updateDialogOpen !== newValue) + { + this.setState({updateDialogOpen: newValue}); + } + } componentDidMount() { super.componentDidMount(); - window.addEventListener("beforeunload", this.unmountListener); + window.addEventListener("beforeunload", this.beforeUnloadListener); + window.addEventListener("unload", this.unloadListener); this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_); this.model_.state.addOnChangedHandler(this.stateChangeHandler_); @@ -529,6 +542,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler); this.model_.banks.addOnChangedHandler(this.banksChangedHandler); this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler); + this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler); this.alertMessageChangedHandler(); } @@ -550,14 +564,17 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< componentWillUnmount() { super.componentWillUnmount(); - window.removeEventListener("beforeunload", this.unmountListener); + window.removeEventListener("beforeunload", this.beforeUnloadListener); + this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler); this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_); this.model_.state.removeOnChangedHandler(this.stateChangeHandler_); this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler); this.model_.banks.removeOnChangedHandler(this.banksChangedHandler); this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler); + this.model_.close(); + } alertMessageChangedHandler() { @@ -661,6 +678,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< return "Applying\u00A0changes..."; case State.ReloadingPlugins: return "Reloading\u00A0plugins..."; + case State.DownloadingUpdate: + return "Downloading update..."; + case State.InstallingUpdate: + return "Installing update...."; default: return "Reconnecting..."; } @@ -787,19 +808,19 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< - + { this.handleDrawerSaveBankAs() }} > - + { this.handleDrawerManageBanks(); }}> - + @@ -840,7 +861,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.setState({ bankDialogOpen: false })} /> - this.setState({ aboutDialogOpen: false })} /> + { (this.state.aboutDialogOpen)&& + ( + this.setState({ aboutDialogOpen: false })} /> + )} { this.model_.zoomedUiControl.set(undefined); } } /> + + {this.state.showStatusMonitor && ()} + + + - {this.state.showStatusMonitor && ()} +
diff --git a/react/src/BankDialog.tsx b/react/src/BankDialog.tsx index eb61160..94ce27b 100644 --- a/react/src/BankDialog.tsx +++ b/react/src/BankDialog.tsx @@ -24,11 +24,11 @@ import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { BankIndexEntry, BankIndex } from './Banks'; import Button from "@mui/material/Button"; import ButtonBase from "@mui/material/ButtonBase"; -import Slide, {SlideProps} from '@mui/material/Slide'; +import Slide, { SlideProps } from '@mui/material/Slide'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import { Theme, createStyles } from '@mui/material/styles'; -import { WithStyles,withStyles } from "@mui/styles"; +import { WithStyles, withStyles } from "@mui/styles"; import DraggableGrid, { ScrollDirection } from './DraggableGrid'; import Fade from '@mui/material/Fade'; @@ -46,8 +46,12 @@ import MenuItem from '@mui/material/MenuItem'; import DialogEx from './DialogEx'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; -import { ReactComponent as DownloadIcon} from './svg/file_download_black_24dp.svg'; -import { ReactComponent as UploadIcon} from './svg/file_upload_black_24dp.svg'; +import { ReactComponent as DownloadIcon } from './svg/file_download_black_24dp.svg'; +import { ReactComponent as UploadIcon } from './svg/file_upload_black_24dp.svg'; +import { ReactComponent as BankIcon } from './svg/ic_bank.svg'; +//import PublishIcon from '@mui/icons-material/Publish'; +// import FileDownloadIcon from '@mui/icons-material/FileDownload'; +// import AppsIcon from '@mui/icons-material/Apps'; interface BankDialogProps extends WithStyles { show: boolean; @@ -73,8 +77,8 @@ interface BankDialogState { const styles = (theme: Theme) => createStyles({ listIcon: { - width: 24, height: 24, - opacity: 0.6, fill: theme.palette.text.primary + width: 24, height: 24, + opacity: 0.6, fill: theme.palette.text.primary }, dialogAppBar: { position: 'relative', @@ -166,7 +170,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( uploadAfter = await this.model.uploadBank(fileList[i], uploadAfter); } } catch (error) { - this.model.showAlert(error +""); + this.model.showAlert(error + ""); }; return uploadAfter; } @@ -182,7 +186,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( }) .catch(err => { e.target.value = ""; // clear the file list so we can get another change notice. - this.model.showAlert(err); + this.model.showAlert(err.toString()); }); } handleUploadBank() { @@ -266,7 +270,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( }); }) .catch((error) => { - this.model.showAlert(error); + this.model.showAlert(error.toString()); }); } } @@ -300,11 +304,11 @@ const BankDialog = withStyles(styles, { withTheme: true })( >
-
- +
+
- + {bankEntry.name}
@@ -324,7 +328,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( }); this.model.moveBank(from, to) .catch((error) => { - this.model.showAlert(error); + this.model.showAlert(error.toString()); }); } @@ -405,8 +409,8 @@ const BankDialog = withStyles(styles, { withTheme: true })( return ( { this.handleDialogClose() }} TransitionComponent={Transition} - style={{userSelect: "none"}} - > + style={{ userSelect: "none" }} + >
@@ -482,8 +486,8 @@ const BankDialog = withStyles(styles, { withTheme: true })( > { this.handleDownloadBank(); }} > - + Download bank @@ -494,9 +498,8 @@ const BankDialog = withStyles(styles, { withTheme: true })( this.handleUploadBank()}> - - + + Upload bank @@ -541,15 +544,15 @@ const BankDialog = withStyles(styles, { withTheme: true })( /> this.handleDeletePromptClose()} - style={{userSelect: "none"}}> + style={{ userSelect: "none" }}> - Are you sure you want to delete bank '{this.getSelectedBankName()}'? + Are you sure you want to delete bank '{this.getSelectedBankName()}'? - - diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index 9105244..b2ca5d2 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -349,7 +349,7 @@ export default withStyles(styles, { withTheme: true })( } ).catch( (e: any) => { - this.model.showAlert(e + ""); + this.model.showAlert(e.toString()); } ); } diff --git a/react/src/JackHostStatus.tsx b/react/src/JackHostStatus.tsx index adcde7e..359671c 100644 --- a/react/src/JackHostStatus.tsx +++ b/react/src/JackHostStatus.tsx @@ -138,7 +138,7 @@ export default class JackHostStatus { {label} - {status.errorMessage === "" ? "Stopped" : status.errorMessage}   + {status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage}   { status.temperaturemC > -100000 && diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 89e73b3..7c90d36 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -20,7 +20,7 @@ import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin'; import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError'; - +import { UpdateStatus, UpdatePolicyT } from './Updater'; import { ObservableProperty } from './ObservableProperty'; import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard' import PluginClass from './PluginClass'; @@ -38,7 +38,7 @@ import GovernorSettings from './GovernorSettings'; import WifiChannel from './WifiChannel'; import AlsaDeviceInfo from './AlsaDeviceInfo'; import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost'; -import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode'; +import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; @@ -49,13 +49,20 @@ export enum State { Background, Reconnecting, ApplyingChanges, - ReloadingPlugins + ReloadingPlugins, + DownloadingUpdate, + InstallingUpdate, }; +export function wantsLoadingScreen(state: State) { + return state >= State.Reconnecting; +} + export enum ReconnectReason { Disconnected, LoadingSettings, ReloadingPlugins, + Updating, }; export type ControlValueChangedHandler = (key: string, value: number) => void; @@ -369,6 +376,9 @@ export class PiPedalModel //implements PiPedalModel state: ObservableProperty = new ObservableProperty(State.Loading); visibilityState: ObservableProperty = new ObservableProperty(VisibilityState.Visible); + promptForUpdate: ObservableProperty = new ObservableProperty(false); + updateStatus: ObservableProperty = new ObservableProperty(new UpdateStatus()); + errorMessage: ObservableProperty = new ObservableProperty(""); alertMessage: ObservableProperty = new ObservableProperty(""); @@ -431,23 +441,31 @@ export class PiPedalModel //implements PiPedalModel this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this); } - onSocketReconnecting(retry: number, maxRetries: number): void { - if (this.visibilityState.get() === VisibilityState.Hidden) return; + expectDisconnect(reason: ReconnectReason) { + this.reconnectReason = reason; + } + + onSocketReconnecting(retry: number, maxRetries: number): boolean { + if (this.isClosed) return false; + if (this.visibilityState.get() === VisibilityState.Hidden) return false; //if (retry !== 0) { - switch (this.reconnectReason) - { - case ReconnectReason.Disconnected: - default: - this.setState(State.Reconnecting); - break; - case ReconnectReason.LoadingSettings: - this.setState(State.ApplyingChanges); - break; - case ReconnectReason.ReloadingPlugins: - this.setState(State.ReloadingPlugins); - break; + switch (this.reconnectReason) { + case ReconnectReason.Disconnected: + default: + this.setState(State.Reconnecting); + break; + case ReconnectReason.LoadingSettings: + this.setState(State.ApplyingChanges); + break; + case ReconnectReason.ReloadingPlugins: + this.setState(State.ReloadingPlugins); + break; + case ReconnectReason.Updating: + this.setState(State.InstallingUpdate); + break; } + return true; } @@ -486,17 +504,17 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onOutputVolumeChanged") { let value = body as number; - this._setOutputVolume(value,false); + this._setOutputVolume(value, false); } else if (message === "onInputVolumeChanged") { let value = body as number; - this._setInputVolume(value,false); + this._setInputVolume(value, false); } else if (message === "onLv2StateChanged") { let instanceId = body.instanceId as number; - let state = body.state as [boolean,any]; + let state = body.state as [boolean, any]; - this.onLv2StateChanged(instanceId,state); + this.onLv2StateChanged(instanceId, state); } else if (message === "onVst3ControlChanged") { let controlChangedBody = body as Vst3ControlChangedBody; this._setVst3PedalboardControlValue( @@ -613,18 +631,113 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onSystemMidiBindingsChanged") { let bindings = MidiBinding.deserialize_array(body); this.systemMidiBindings.set(bindings); - } else if (message === "onErrorMessage") - { + } else if (message === "onErrorMessage") { this.showAlert(body as string); - } - else if (message === "onLv2PluginsChanging") - { + } + else if (message === "onLv2PluginsChanging") { this.onLv2PluginsChanging(); + } else if (message === "onUpdateStatusChanged") { + let updateStatus = new UpdateStatus().deserialize(body); + this.onUpdateStatusChanged(updateStatus); } } - onLv2PluginsChanging() : void { - this.reconnectReason = ReconnectReason.ReloadingPlugins; + + private updateLaterTimeout?: NodeJS.Timeout = undefined; + + private clearPromptForUpdateTimer() { + if (this.updateLaterTimeout) { + clearTimeout(this.updateLaterTimeout); + this.updateLaterTimeout = undefined; + } + } + + private setPromptForUpdateTimer(when: Date) { + let ms = when.getTime() - Date.now(); + this.updateLaterTimeout = setTimeout( + () => { + this.updateLaterTimeout = undefined; + // make the server do a fresh check + this.getUpdateStatus() + .then( + () => { + this.updatePromptForUpdate(); + }) + .catch((e) => { + + }); + }, + ms); + } + + + private lastCanUpdateNow: boolean = false; + private updatePromptForUpdate() { + this.clearPromptForUpdateTimer(); + + let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready; + let timeEnabled = false; + + let updateLaterTime = this.getUpdateTime(); + if (updateLaterTime == null) { + timeEnabled = true; + } else { + let nDate = Date.now(); + + let now: Date = new Date(nDate); + let maxDate: Date = new Date(nDate + 86400000*2); // sanity check for systems with unstable system clock + + timeEnabled = (updateLaterTime < now || updateLaterTime >= maxDate) + } + let updateStatus = this.updateStatus.get(); + let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable; + + let canUpdateNow: boolean = (stateEnabled && timeEnabled && statusEnabled); + if (updateStatus.updatePolicy === UpdatePolicyT.Disable) + { + canUpdateNow = false; + } + + if (canUpdateNow && canUpdateNow !== this.lastCanUpdateNow) + { + this.showUpdateDialogValue = true; // make the dialog sticky so it can show OK button + } + this.lastCanUpdateNow = canUpdateNow; + this.promptForUpdate.set(this.showUpdateDialogValue || canUpdateNow); + + if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime) { + this.setPromptForUpdateTimer(updateLaterTime); + } + } + private showUpdateDialogValue: boolean = false; + + showUpdateDialog(show: boolean = true) { + if (this.showUpdateDialogValue !== show) { + this.showUpdateDialogValue = show; + if (show) { + this.forceUpdateCheck(); + } + this.updatePromptForUpdate(); + } + } + + onUpdateStatusChanged(updateStatus: UpdateStatus): void { + let current = this.updateStatus.get(); + if (!current.equals(updateStatus)) { + this.updateStatus.set(updateStatus); + if (current.currentVersion.length !== 0 && current.currentVersion !== updateStatus.currentVersion) { + // !! Server has been updated!!! + this.reloadPage(); + throw new Error("Reloading..."); + } + if (updateStatus.getActiveRelease().updateAvailable) { + this.promptForUpdate.set(true); + } + this.updatePromptForUpdate() + } + } + onLv2PluginsChanging(): void { + this.expectDisconnect(ReconnectReason.ReloadingPlugins); // this.webSocket?.reconnect(); // let the server do it for us. } @@ -642,16 +755,17 @@ export class PiPedalModel //implements PiPedalModel } } - validatePlugins(plugin: PluginClass) { + validatePluginClasses(plugin: PluginClass) { if (plugin.plugin_type === PluginType.None) { console.log("Error: No plugin type for uri '" + plugin.uri + "'"); } for (let i = 0; i < plugin.children.length; ++i) { - this.validatePlugins(plugin.children[i]); + this.validatePluginClasses(plugin.children[i]); } } + private reconnectReason: ReconnectReason = ReconnectReason.Disconnected; isReloading(): boolean { @@ -667,7 +781,6 @@ export class PiPedalModel //implements PiPedalModel onSocketConnectionLost() { // remove all the events and subscriptions we have. - // yyy this.vuSubscriptions = []; this.monitorPatchPropertyListeners = []; @@ -681,13 +794,17 @@ export class PiPedalModel //implements PiPedalModel this.androidHost?.setDisconnected(false); } - this.reconnectReason = ReconnectReason.Disconnected; + this.expectDisconnect(ReconnectReason.Disconnected); // the next expected disconnect will be an actual disconnect. if (this.visibilityState.get() === VisibilityState.Hidden) return; // reload state, but not configuration. this.getWebSocket().request("hello") .then(clientId => { this.clientId = clientId; + return this.getUpdateStatus(); // detects whether server has been upgraded. + }) + .then((updateStatus) => { + return this.getWebSocket().request("plugins"); }) .then(data => { @@ -762,7 +879,7 @@ export class PiPedalModel //implements PiPedalModel this.setState(State.Ready); }) .catch((what) => { - this.onError(what); + this.onError(what.toString()); }) } makeSocketServerUrl(hostName: string, port: number): string { @@ -774,7 +891,7 @@ export class PiPedalModel //implements PiPedalModel } - maxFileUploadSize: number = 512*1024*1024; + maxFileUploadSize: number = 512 * 1024 * 1024; maxPresetUploadSize: number = 1024 * 1024; debug: boolean = false; @@ -795,7 +912,7 @@ export class PiPedalModel //implements PiPedalModel this.androidHost = new FakeAndroidHost(); } this.debug = !!data.debug; - let { socket_server_port, socket_server_address,max_upload_size } = data; + let { socket_server_port, socket_server_address, max_upload_size } = data; if ((!socket_server_address) || socket_server_address === "*") { socket_server_address = window.location.hostname; } @@ -825,6 +942,9 @@ export class PiPedalModel //implements PiPedalModel return false; }) .then(() => { + return this.getUpdateStatus(); + }) + .then((updateStatus) => { const isoRequest = new Request('iso_codes.json'); return fetch(isoRequest); }) @@ -846,7 +966,7 @@ export class PiPedalModel //implements PiPedalModel return true; }) .catch((error) => { - this.setError("Failed to connect to server. " + error); + this.setError("Failed to connect to server. " + error.toString()); return false; }) .then((succeeded) => { @@ -868,7 +988,7 @@ export class PiPedalModel //implements PiPedalModel .then((data) => { this.plugin_classes.set(new PluginClass().deserialize(data)); - this.validatePlugins(this.plugin_classes.get()); + this.validatePluginClasses(this.plugin_classes.get()); return this.getWebSocket().request("getPresets"); }) @@ -941,7 +1061,7 @@ export class PiPedalModel //implements PiPedalModel return true; }) .catch((error) => { - this.setError("Failed to fetch server state.\n\n" + error); + this.setError("Failed to fetch server state.\n\n" + error.toString()); return false; }); @@ -1025,7 +1145,7 @@ export class PiPedalModel //implements PiPedalModel } }) .catch((error) => { - this.setError("Failed to get server state. \n\n" + error); + this.setError("Failed to get server state. \n\n" + error.toString()); }); let t = this.onVisibilityChanged; @@ -1131,7 +1251,7 @@ export class PiPedalModel //implements PiPedalModel } } - private onLv2StateChanged(instanceId: number, state: [boolean,any]): void { + private onLv2StateChanged(instanceId: number, state: [boolean, any]): void { let item = this.pedalboard.get().getItem(instanceId); item.lv2State = state; for (let item of this.stateChangedListeners) { @@ -1182,33 +1302,31 @@ export class PiPedalModel //implements PiPedalModel this.webSocket?.send("setPatchProperty", body); } - setInputVolume(volume_db: number) : void { - this._setInputVolume(volume_db,true); + setInputVolume(volume_db: number): void { + this._setInputVolume(volume_db, true); } - previewInputVolume(volume_db: number) : void { - nullCast(this.webSocket).send("previewInputVolume",volume_db); + previewInputVolume(volume_db: number): void { + nullCast(this.webSocket).send("previewInputVolume", volume_db); } - previewOutputVolume(volume_db: number) : void { - nullCast(this.webSocket).send("previewOutputVolume",volume_db); + previewOutputVolume(volume_db: number): void { + nullCast(this.webSocket).send("previewOutputVolume", volume_db); } - private _setInputVolume(volume_db: number, notifyServer: boolean) : void { - let changed: boolean = false; + private _setInputVolume(volume_db: number, notifyServer: boolean): void { + let changed: boolean = false; let pedalboard = this.pedalboard.get(); - if (pedalboard.input_volume_db !== volume_db) - { + if (pedalboard.input_volume_db !== volume_db) { let newPedalboard = pedalboard.clone(); newPedalboard.input_volume_db = volume_db; this.pedalboard.set(newPedalboard); changed = true; } - if (changed) - { + if (changed) { if (notifyServer) { - nullCast(this.webSocket).send("setInputVolume",volume_db); + nullCast(this.webSocket).send("setInputVolume", volume_db); } for (let i = 0; i < this._controlValueChangeItems.length; ++i) { @@ -1220,26 +1338,23 @@ export class PiPedalModel //implements PiPedalModel } } - setOutputVolume(volume_db: number, notifyServer: boolean) : void - { - this._setOutputVolume(volume_db,true); + setOutputVolume(volume_db: number, notifyServer: boolean): void { + this._setOutputVolume(volume_db, true); } - private _setOutputVolume(volume_db: number, notifyServer: boolean) : void { - let changed: boolean = false; + private _setOutputVolume(volume_db: number, notifyServer: boolean): void { + let changed: boolean = false; let pedalboard = this.pedalboard.get(); - if (pedalboard.output_volume_db !== volume_db) - { + if (pedalboard.output_volume_db !== volume_db) { let newPedalboard = pedalboard.clone(); newPedalboard.output_volume_db = volume_db; this.pedalboard.set(newPedalboard); changed = true; } - if (changed) - { + if (changed) { if (notifyServer) { - nullCast(this.webSocket).send("setOutputVolume",volume_db); + nullCast(this.webSocket).send("setOutputVolume", volume_db); } for (let i = 0; i < this._controlValueChangeItems.length; ++i) { let item = this._controlValueChangeItems[i]; @@ -1256,15 +1371,13 @@ export class PiPedalModel //implements PiPedalModel if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready."); let newPedalboard = pedalboard.clone(); - if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") - { - this._setInputVolume(value,notifyServer); + if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") { + this._setInputVolume(value, notifyServer); return; - } else if (instanceId === Pedalboard.END_CONTROL) - { - this._setOutputVolume(value,notifyServer); + } else if (instanceId === Pedalboard.END_CONTROL) { + this._setOutputVolume(value, notifyServer); return; - } + } let item = newPedalboard.getItem(instanceId); let changed = item.setControlValue(key, value); @@ -1365,7 +1478,7 @@ export class PiPedalModel //implements PiPedalModel while (true) { let v = it.next(); if (v.done) break; - let item = v.value; + let item = v.value; if (item.instanceId === itemId) { item.deserialize(new PedalboardItem()); // skeezy way to re-initialize. item.instanceId = ++newPedalboard.nextInstanceId; @@ -1395,16 +1508,14 @@ export class PiPedalModel //implements PiPedalModel // mouse is down. Don't update EVERYBODY, but we must change // the control on the running audio plugin. // TODO: respect "expensive" port attribute. - if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") - { + if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") { this.previewInputVolume(value); return; - } else if (instanceId === Pedalboard.END_CONTROL) - { + } else if (instanceId === Pedalboard.END_CONTROL) { this.previewOutputVolume(value); return; - } - + } + this._setServerControl("previewControl", instanceId, key, value); } @@ -1530,13 +1641,11 @@ export class PiPedalModel //implements PiPedalModel } addPedalboardItem(instanceId: number, append: boolean): number { let pedalboard = this.pedalboard.get(); - if (instanceId === Pedalboard.START_CONTROL && append) - { + if (instanceId === Pedalboard.START_CONTROL && append) { instanceId = pedalboard.items[0].instanceId; append = false; - } else if (instanceId === Pedalboard.END_CONTROL && !append) - { - instanceId = pedalboard.items[pedalboard.items.length-1].instanceId; + } else if (instanceId === Pedalboard.END_CONTROL && !append) { + instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId; append = true; } let newPedalboard = pedalboard.clone(); @@ -1555,14 +1664,12 @@ export class PiPedalModel //implements PiPedalModel } addPedalboardSplitItem(instanceId: number, append: boolean): number { let pedalboard = this.pedalboard.get(); - - if (instanceId === Pedalboard.START_CONTROL && append) - { + + if (instanceId === Pedalboard.START_CONTROL && append) { instanceId = pedalboard.items[0].instanceId; append = false; - } else if (instanceId === Pedalboard.END_CONTROL && !append) - { - instanceId = pedalboard.items[pedalboard.items.length-1].instanceId; + } else if (instanceId === Pedalboard.END_CONTROL && !append) { + instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId; append = true; } @@ -1615,7 +1722,6 @@ export class PiPedalModel //implements PiPedalModel } - saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise { // default behaviour is to save after the currently selected preset. if (saveAfterInstanceId === -1) { @@ -1636,21 +1742,97 @@ export class PiPedalModel //implements PiPedalModel }); } + getUpdateStatus(): Promise { + return new Promise( + (accept, reject) => { + nullCast(this.webSocket) + .request('getUpdateStatus') + .then((result) => { + let updateStatus = new UpdateStatus().deserialize(result); + this.onUpdateStatusChanged(updateStatus); + accept(result); + }).catch( + (e) => { + reject(e); + } + + ); + + }); + + } + launchExternalUrl(url: string) { + if (this.isAndroidHosted()) { + try { + this.androidHost?.launchExternalUrl(url); + } catch (e) { + // if they haven't updated their client yet, just don't do it. + } + + + } else { + window.open(url, "_blank")?.focus(); + } + } + + updateLater(delayMs: number) { + let futureDate = new Date(Date.now() + delayMs); + localStorage.setItem('nextUpdateTime', futureDate.toISOString()); + + this.updatePromptForUpdate(); + } + + updateNow(): Promise { + return new Promise( + (accept, reject) => { + let updateStatus = this.updateStatus.get(); + if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable) { + this.setState(State.DownloadingUpdate); + this.expectDisconnect(ReconnectReason.Updating); + let url = updateStatus.getActiveRelease().updateUrl; + + nullCast(this.webSocket) + .request('updateNow', url) + .then(() => { + this.setState(State.InstallingUpdate); + accept(); + }) + .catch( + (e: any) => { + this.expectDisconnect(ReconnectReason.Disconnected); + + this.setState(State.Ready); // TODO: hopefully we haven't had an intermediate disconnect. + reject(e); + }); + } else { + reject(new Error("Invalid update request.")); + } + + } + ); + } + + getUpdateTime(): Date | null { + let item = localStorage.getItem('nextUpdateTime'); + if (item) { + return new Date(item); + } + return null; + } // deprecated. requestFileList(piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) .request('requestFileList', piPedalFileProperty); } - requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise { + requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) .request('requestFileList2', - {relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty} + { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } ); } - - deleteUserFile(fileName: string) : Promise - { - return nullCast(this.webSocket).request('deleteUserFile',fileName); + + deleteUserFile(fileName: string): Promise { + return nullCast(this.webSocket).request('deleteUserFile', fileName); } @@ -1712,13 +1894,14 @@ export class PiPedalModel //implements PiPedalModel return this.copyPreset(instanceId, -1); } - showAlert(message: string): void { - this.alertMessage.set(message); + showAlert(message: string| Error): void { + let m = message.toString(); + this.alertMessage.set(m); } setJackSettings(jackSettings: JackChannelSelection): void { - this.reconnectReason = ReconnectReason.LoadingSettings; + this.expectDisconnect(ReconnectReason.LoadingSettings); this.webSocket?.send("setJackSettings", jackSettings); } @@ -1823,7 +2006,14 @@ export class PiPedalModel //implements PiPedalModel } } - + private isClosed = false; + close() { + if (!this.isClosed) { + this.isClosed = true; + this.webSocket?.close(); + this.webSocket = undefined; + } + } setPatchProperty(instanceId: number, uri: string, value: any): Promise { let result = new Promise((resolve, reject) => { if (this.webSocket) { @@ -2107,7 +2297,7 @@ export class PiPedalModel //implements PiPedalModel window.open(url, "_blank"); } - uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { + uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { let result = new Promise((resolve, reject) => { try { if (file.size > this.maxFileUploadSize) { @@ -2117,17 +2307,15 @@ export class PiPedalModel //implements PiPedalModel let parsedUrl = new URL(url); let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop(); let query = parsedUrl.search; - if (query.length === 0) - { + if (query.length === 0) { query += '?'; } else { query += '&'; } - if (!fileNameOnly) - { + if (!fileNameOnly) { reject("Invalid filename."); } - query += "filename=" + encodeURIComponent(fileNameOnly??""); + query += "filename=" + encodeURIComponent(fileNameOnly ?? ""); parsedUrl.search = query; url = parsedUrl.toString(); fetch( @@ -2146,7 +2334,7 @@ export class PiPedalModel //implements PiPedalModel reject("Upload failed. " + response.statusText); return; } else { - return response.json(); + return response.json(); } }) .then((json) => { @@ -2195,7 +2383,7 @@ export class PiPedalModel //implements PiPedalModel resolve(json as number); }) .catch((error) => { - reject("Upload failed. " + error); + reject("Upload failed. " + error.toString()); }) ; } catch (error) { @@ -2254,8 +2442,7 @@ export class PiPedalModel //implements PiPedalModel return new Promise((resolve, reject) => { let ws = this.webSocket; - if (!ws) - { + if (!ws) { resolve(); return; } @@ -2271,8 +2458,7 @@ export class PiPedalModel //implements PiPedalModel }); }); } - createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise - { + createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; @@ -2295,8 +2481,8 @@ export class PiPedalModel //implements PiPedalModel }); }); } - - getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise { + + getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { @@ -2306,15 +2492,14 @@ export class PiPedalModel //implements PiPedalModel ws.request( "getFilePropertyDirectoryTree", uiFileProperty - ).then((result)=> { + ).then((result) => { resolve(new FilePropertyDirectoryTree().deserialize(result)); }).catch((e) => { reject(e); }); }); } - renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise - { + renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; @@ -2441,11 +2626,11 @@ export class PiPedalModel //implements PiPedalModel .catch((err) => { //resolve(); }); - resolve(); + resolve(); }); - this.reconnectReason = ReconnectReason.LoadingSettings; - this.webSocket?.reconnect(); // close immediately, and wait for recoonnect. + this.expectDisconnect(ReconnectReason.LoadingSettings); + // this.webSocket?.reconnect(); // avoid races by letting the server do it for us. return result; } @@ -2524,8 +2709,7 @@ export class PiPedalModel //implements PiPedalModel ++ix; if (ix >= uiPlugin.controls.length) return; - while (!uiPlugin.controls[ix].is_input) - { + while (!uiPlugin.controls[ix].is_input) { ++ix; if (ix >= uiPlugin.controls.length) return; } @@ -2558,8 +2742,7 @@ export class PiPedalModel //implements PiPedalModel --ix; if (ix < 0) return; - while (!uiPlugin.controls[ix].is_input) - { + while (!uiPlugin.controls[ix].is_input) { --ix; if (ix < 0) return; } @@ -2590,6 +2773,20 @@ export class PiPedalModel //implements PiPedalModel } + setUpdatePolicy(updatePolicy: UpdatePolicyT): void { + let iPolicy = updatePolicy as number; + if (this.webSocket) { + this.webSocket.send("setUpdatePolicy", iPolicy); + } + + } + forceUpdateCheck() { + if (this.webSocket) { + this.webSocket.send("forceUpdateCheck"); + } + + } + preloadImages(imageList: string): void { let imageNames = imageList.split(';'); for (let i = 0; i < imageNames.length; ++i) { @@ -2619,8 +2816,7 @@ export class PiPedalModel //implements PiPedalModel } // returns the ID of the new preset. - newPresetItem(createAfter: number): Promise - { + newPresetItem(createAfter: number): Promise { return nullCast(this.webSocket).request("newPreset"); } @@ -2630,14 +2826,13 @@ export class PiPedalModel //implements PiPedalModel setTheme(value: ColorTheme) { - if (this.getTheme() !== value) - { + if (this.getTheme() !== value) { setColorScheme(value); - setTimeout(()=>{ + setTimeout(() => { this.reloadPage(); }, - 200); -} + 200); + } } @@ -2647,7 +2842,6 @@ export class PiPedalModel //implements PiPedalModel window.location.href = url; //window.location.reload(); } - }; let instance: PiPedalModel | undefined = undefined; diff --git a/react/src/PiPedalSocket.tsx b/react/src/PiPedalSocket.tsx index 6564cba..8a10bea 100644 --- a/react/src/PiPedalSocket.tsx +++ b/react/src/PiPedalSocket.tsx @@ -41,7 +41,7 @@ export interface PiPedalSocketListener { onError: (message: string, exception?: Error) => void; onConnectionLost: () => void; onReconnect: () => void; - onReconnecting: (retry: number, maxRetries: number) => void; + onReconnecting: (retry: number, maxRetries: number) => boolean; }; class PiPedalSocket { @@ -183,6 +183,7 @@ class PiPedalSocket { handleClose(_event: any): any { if (this.retrying) { + this.close(); // treat this as a fatal error. if (this.listener) { this.listener.onError("Server connection lost."); @@ -230,7 +231,9 @@ class PiPedalSocket { this.close(); } - this.listener.onReconnecting(this.retryCount,MAX_RETRIES); + if (!this.listener.onReconnecting(this.retryCount,MAX_RETRIES)) { + return; + } ++this.retryCount; this.connectInternal_() @@ -262,6 +265,7 @@ class PiPedalSocket { this.socket.onmessage = null; this.socket.onopen = null; this.socket.close(); + this.socket = undefined; } } catch (ignored) { diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index da24e27..64a6dd1 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -257,6 +257,9 @@ const PluginControl = touchDown: boolean = false; touchIdentifier: number = -1; + private lastclickTimeMs: number = 0; + private isTap: boolean = false; + onTouchStart(e: TouchEvent) { } onTouchMove(e: TouchEvent) { @@ -272,6 +275,7 @@ const PluginControl = let e = e_ as PointerEvent; if (this.isExtraTouch(e)) { + this.isTap = false; this.captureElement!.setPointerCapture(e.pointerId); this.capturedPointers.push(e.pointerId); ++this.pointersDown; @@ -305,6 +309,7 @@ const PluginControl = { if (this.props.uiControl?.isDial()??false) { + this.isTap = false; this.showZoomedControl(); return; } @@ -312,8 +317,14 @@ const PluginControl = ++this.pointersDown; - this.mouseDown = true; + if (this.pointersDown === 1) + { + this.isTap = true; + this.tapStartMs = Date.now(); + } else { + this.isTap = false; + } this.pointerId = e.pointerId; this.pointerType = e.pointerType; @@ -342,6 +353,7 @@ const PluginControl = if (this.isExtraTouch(e)) { ++this.pointersDown; + this.isTap = false; } } @@ -354,6 +366,7 @@ const PluginControl = onPointerLostCapture(e: PointerEvent) { if (this.isCapturedPointer(e)) { --this.pointersDown; + this.isTap = false; this.releaseCapture(e); @@ -396,6 +409,35 @@ const PluginControl = this.lastX = e.clientX; return this.dRange; } + private lastTapMs = 0; + + resetToDefaultValue(uiControl: UiControl): void + { + let value = uiControl.default_value; + this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,value); + } + onPointerDoubleTap() { + let uiControl = this.props.uiControl; + if (uiControl) + { + if (uiControl.isDial()) + { + this.resetToDefaultValue(uiControl); + } + } + } + onPointerTap() { + let tapTime = Date.now(); + let dT = tapTime-this.lastTapMs; + this.lastTapMs = tapTime; + + if (dT < 500) + { + this.onPointerDoubleTap(); + } + } + private tapStartMs: number = 0; + onPointerUp(e: PointerEvent) { if (this.isCapturedPointer(e)) { @@ -407,6 +449,14 @@ const PluginControl = this.previewRange(dRange, true); this.releaseCapture(e); + if (this.isTap) + { + let ms = Date.now()-this.tapStartMs; + if (ms < 200) + { + this.onPointerTap(); + } + } } else { --this.pointersDown; @@ -437,11 +487,24 @@ const PluginControl = } + clickSlop() { + return 3.5; // maybe larger on touch devices. + } onPointerMove(e: PointerEvent): void { if (this.isCapturedPointer(e)) { e.preventDefault(); let dRange = this.updateRange(e) this.previewRange(dRange, false); + + let x = e.clientX; + let y = e.clientY; + let dx = x - this.startX; + let dy = y-this.startY; + let distance = Math.sqrt(dx*dx+dy*dy); + if (distance >= this.clickSlop()) + { + this.isTap = false; + } } } diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index bd1d863..c8481da 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -393,6 +393,12 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( showThemeSelectDialog: true }); } + + handleCheckForUpdates() + { + this.model.showUpdateDialog(); + } + handleMidiMessageSettings() { this.setState({ showSystemMidiBindingsDialog: true @@ -665,7 +671,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( this.handleMidiMessageSettings()} >
- System MIDI Bindings + System MIDI bindings
@@ -748,7 +754,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
- Color Theme + Color theme { this.model.getTheme() === ColorTheme.Dark ? "Dark" : (this.model.getTheme() === ColorTheme.Light ? "Light": "System")} @@ -786,6 +792,18 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
+ { this.handleCheckForUpdates(); }} > + +
+ + Check for updates... +
+
+ + + this.handleRestart()} > diff --git a/react/src/UpdateDialog.tsx b/react/src/UpdateDialog.tsx new file mode 100644 index 0000000..8223b33 --- /dev/null +++ b/react/src/UpdateDialog.tsx @@ -0,0 +1,285 @@ +/* + * MIT License + * + * Copyright (c) 2022 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. + */ + +import React from 'react'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import DialogEx from './DialogEx'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import { UpdateStatus, UpdateRelease, UpdatePolicyT, intToUpdatePolicyT } from './Updater'; +import { PiPedalModelFactory, PiPedalModel } from './PiPedalModel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; + + +const UPDATE_CHECK_DELAY = 86400000; // one day in ms. +// const UPDATE_CHECK_DELAY = 30 * 1000; // testing + + + +export interface UpdateDialogProps { + open: boolean; +}; + +export interface UpdateDialogState { + updateStatus: UpdateStatus; + compactLandscape: boolean; + alertDialogOpen: boolean; + alertDialogMessage: string; +}; + +export default class UpdateDialog extends React.Component { + private model: PiPedalModel; + + constructor(props: UpdateDialogProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + let updateStatus = this.model.updateStatus.get(); + this.state = { + updateStatus: updateStatus, + compactLandscape: false, + alertDialogOpen: false, + alertDialogMessage: "" + }; + this.onUpdateStatusChanged = this.onUpdateStatusChanged.bind(this); + } + + onUpdateStatusChanged(newValue: UpdateStatus) { + if (!newValue.equals(this.state.updateStatus)) { + this.setState({ updateStatus: newValue }); + } + } + + private mounted: boolean = false; + + componentDidMount() { + this.mounted = true; + this.model.updateStatus.addOnChangedHandler(this.onUpdateStatusChanged); + } + componentWillUnmount() { + this.model.updateStatus.removeOnChangedHandler(this.onUpdateStatusChanged); + this.mounted = false; + + } + + + private handleOK() { + this.model.showUpdateDialog(false); + } + private handleUpdateNow() { + this.model.updateNow() + .then(() => { }) // all handling is done by model, so we can run ui-less. + .catch((e: any) => { + this.showAlert(e.toString()); + }); + } + private handleUpdateLater() { + this.model.updateLater(UPDATE_CHECK_DELAY); + this.model.showUpdateDialog(false); // allow close if launched from the settings window. + } + + private handleClose() { + // ideally, we'd like to not close at all, but it screws up the nav backstack if we dont. + // so, close, but do so very briefly + if (this.canUpgrade()) { + this.model.updateLater(UPDATE_CHECK_DELAY); // close and re-open immediately. + } + this.model.showUpdateDialog(false); + } + + handleCloseAlert() { + this.setState({ alertDialogOpen: false, alertDialogMessage: "" }) + } + + showAlert(message: string) { + this.setState({ alertDialogOpen: true, alertDialogMessage: message }); + } + + onViewReleaseNotes() { + this.model.launchExternalUrl("https://rerdavies.github.io/pipedal/ReleaseNotes.html"); + } + + upToDate(): boolean { + let updateStatus = this.state.updateStatus; + let updateRelease: UpdateRelease = updateStatus.getActiveRelease(); + return !updateRelease.updateAvailable + } + canUpgrade(): boolean { + let updateStatus = this.state.updateStatus; + if (updateStatus.updatePolicy === UpdatePolicyT.Disable) { + return false; + } + if (updateStatus.errorMessage !== "") { + return false; + } + let updateRelease = updateStatus.getActiveRelease(); + + return updateStatus.isValid && updateStatus.isOnline && updateRelease.updateAvailable; + } + + private onPolicySelected(newValue: string | number): void { + if (newValue.toString() === "") return; + let updatePolicy = intToUpdatePolicyT(newValue as number); + this.model.setUpdatePolicy(updatePolicy); + + } + render() { + let updateStatus = this.state.updateStatus; + let updateRelease = updateStatus.getActiveRelease(); + let upToDate = this.upToDate(); + let canUpgrade = this.canUpgrade(); + let compact = this.state.compactLandscape; + let showUpgradeVersion = !upToDate && updateStatus.isValid && updateRelease.upgradeVersionDisplayName !== ""; + return ( + { this.handleClose(); }} + style={{ userSelect: "none" }} + > + { + (!compact) && + ( + +
+ PiPedal Updates + +
+ +
+ ) + } + { + (!compact) && + ( + + ) + } + + + {(upToDate) && ( + + PiPedal is up to date. + + ) + } + {(canUpgrade) && ( + + A new version of the PiPedal server is available. Would you like to update now? + + + + ) + } + +
+
+ + Current version: + + {(showUpgradeVersion) && ( + + Update version: + + ) + } +
+
+ + {updateStatus.currentVersionDisplayName} + + {(showUpgradeVersion) && ( + + {updateRelease.upgradeVersionDisplayName} + + ) + } +
+
+ { + (updateStatus.errorMessage.length !== 0) && + ( + + { + updateStatus.errorMessage + } + + + ) + } + +
+ + + { + (canUpgrade) ? + ( +
+ +
+ +
+ + + ) : ( + +
+
+ +
+ ) + } + + + { this.handleCloseAlert(); }} + aria-describedby="Alert Dialog" + > + + + { + this.state.alertDialogMessage + } + + + + + + + + + ); + } +} \ No newline at end of file diff --git a/react/src/Updater.tsx b/react/src/Updater.tsx new file mode 100644 index 0000000..993f836 --- /dev/null +++ b/react/src/Updater.tsx @@ -0,0 +1,123 @@ +// Copyright (c) 2024 Robin 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. + +export enum UpdatePolicyT { + // must match declaration in Updater.hpp + ReleaseOnly = 0, + ReleaseOrBeta = 1, + Development = 2, + Disable = 3, + +}; +export function intToUpdatePolicyT(intValue: number): UpdatePolicyT { + return (Object.values(UpdatePolicyT).find(enumValue => enumValue === intValue)) as UpdatePolicyT; +} +export class UpdateRelease { + deserialize(input: any): UpdateRelease { + this.updateAvailable = input.updateAvailable; + this.upgradeVersion = input.upgradeVersion; + this.upgradeVersionDisplayName = input.upgradeVersionDisplayName; + this.assetName = input.assetName; + this.updateUrl = input.updateUrl; + return this; + } + + + updateAvailable: boolean = false; + upgradeVersion: string = ""; //just the version. + upgradeVersionDisplayName: string = ""; // display name for the version. + assetName: string = ""; // filename only + updateUrl: string = ""; // url from which to download the .deb file. + gpgSignatureUrl: string = ""; + + equals(other: UpdateRelease): boolean { + return (this.updateAvailable === other.updateAvailable) && + (this.upgradeVersion === other.upgradeVersion) && + (this.upgradeVersionDisplayName === other.upgradeVersionDisplayName) && + (this.assetName === other.assetName) && + (this.updateUrl === other.updateUrl) && + (this.gpgSignatureUrl === other.gpgSignatureUrl) + ; + } +}; + +export class UpdateStatus { + lastUpdateTime: number = 0; + isValid: boolean = false; + errorMessage: string = ""; + updatePolicy: UpdatePolicyT = UpdatePolicyT.ReleaseOrBeta; + isOnline: boolean = false; + currentVersion: string = ""; + currentVersionDisplayName: string = ""; + + releaseOnlyRelease: UpdateRelease = new UpdateRelease(); + releaseOrBetaRelease: UpdateRelease = new UpdateRelease(); + devRelease: UpdateRelease = new UpdateRelease(); + + deserialize(input: any): UpdateStatus { + this.lastUpdateTime = input.lastUpdateTime; + this.isValid = input.isValid; + this.errorMessage = input.errorMessage; + this.isOnline = input.isOnline; + this.updatePolicy = intToUpdatePolicyT(input.updatePolicy); + this.currentVersion = input.currentVersion; + this.currentVersionDisplayName = input.currentVersionDisplayName; + this.releaseOnlyRelease = new UpdateRelease().deserialize(input.releaseOnlyRelease); + this.releaseOrBetaRelease = new UpdateRelease().deserialize(input.releaseOrBetaRelease); + this.devRelease = new UpdateRelease().deserialize(input.devRelease); + return this; + } + + static deserialize_array(input: any): UpdateStatus[] { + let result: UpdateStatus[] = []; + for (let i = 0; i < input.length; ++i) { + result[i] = new UpdateStatus().deserialize(input[i]); + } + return result; + } + getActiveRelease(): UpdateRelease { + switch (this.updatePolicy) { + case UpdatePolicyT.Disable: // show available updates in the settings dialog. + case UpdatePolicyT.ReleaseOnly: + return this.releaseOnlyRelease; + case UpdatePolicyT.ReleaseOrBeta: + return this.releaseOrBetaRelease; + case UpdatePolicyT.Development: + return this.devRelease; + default: + throw new Error("Invalid argument."); + } + } + equals(other: UpdateStatus): boolean { + return (this.isValid === other.isValid) + && (this.lastUpdateTime === other.lastUpdateTime) + && (this.errorMessage === other.errorMessage) + && (this.isOnline === other.isOnline) + && (this.currentVersion === other.currentVersion) + && (this.currentVersionDisplayName === other.currentVersionDisplayName) + && (this.releaseOnlyRelease.equals(other.releaseOnlyRelease)) + && (this.releaseOrBetaRelease.equals(other.releaseOrBetaRelease)) + && (this.devRelease.equals(other.devRelease)) + ; + // other properties are contractually dealt with. + + } + + +} \ No newline at end of file diff --git a/react/src/ZoomedDial.tsx b/react/src/ZoomedDial.tsx index d4bfbea..4d168d6 100644 --- a/react/src/ZoomedDial.tsx +++ b/react/src/ZoomedDial.tsx @@ -17,12 +17,12 @@ // 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. -import React, { TouchEvent, PointerEvent, SyntheticEvent } from 'react';import { Theme } from '@mui/material/styles'; +import React, { TouchEvent, PointerEvent, SyntheticEvent } from 'react'; import { Theme } from '@mui/material/styles'; import { WithStyles } from '@mui/styles'; import withStyles from '@mui/styles/withStyles'; import createStyles from '@mui/styles/createStyles'; import { PiPedalModel, PiPedalModelFactory, ZoomedControlInfo } from './PiPedalModel'; -import {ReactComponent as DialIcon} from './svg/fx_dial.svg'; +import { ReactComponent as DialIcon } from './svg/fx_dial.svg'; const SELECTED_OPACITY = 0.8; const DEFAULT_OPACITY = 0.6; @@ -36,7 +36,7 @@ const DEAD_ZONE_SIZE = 5; const styles = (theme: Theme) => createStyles({ dialIcon: { - overscrollBehavior: "none", touchAction: "none", + overscrollBehavior: "none", touchAction: "none", fill: theme.palette.text.primary, opacity: DEFAULT_OPACITY, @@ -49,7 +49,7 @@ interface ZoomedDialProps extends WithStyles { size: number, controlInfo: ZoomedControlInfo | undefined, - + onDoubleTap(): void, onPreviewValue(value: number): void, onSetValue(value: number): void } @@ -81,12 +81,28 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( this.onPointerLostCapture = this.onPointerLostCapture.bind(this); } + onPointerDoubleTap() { + this.props.onDoubleTap(); + } + + private lastTapMs: number = 0; + + onPointerTap() { + let tapTime = Date.now(); + let dT = tapTime-this.lastTapMs; + this.lastTapMs = tapTime; + + if (dT < 500) + { + this.onPointerDoubleTap(); + } + } onPointerUp(e: PointerEvent) { if (this.isCapturedPointer(e)) { --this.pointersDown; - + e.preventDefault(); this.updateAngle(e); @@ -94,17 +110,25 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( this.releaseCapture(e); + if (this.isTap) { + let ms = Date.now() - this.tapStartMs; + if (ms < 200) { + this.onPointerTap(); + } + } + + + } else { --this.pointersDown; - + } } - releaseCapture(e: PointerEvent) - { + releaseCapture(e: PointerEvent) { let img = this.imgRef.current; - + if (img && img.style) { img.releasePointerCapture(e.pointerId); img.style.opacity = "" + DEFAULT_OPACITY; @@ -117,8 +141,8 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( } document.body.removeEventListener( "pointerdown", - this.onBodyPointerDownCapture,true - ); + this.onBodyPointerDownCapture, true + ); this.mouseDown = false; @@ -128,7 +152,7 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( onPointerLostCapture(e: PointerEvent) { if (this.isCapturedPointer(e)) { --this.pointersDown; - + this.releaseCapture(e); } @@ -193,7 +217,7 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( let uiControl = this.props.controlInfo.uiControl; - return r * (uiControl.max_value-uiControl.min_value)+uiControl.min_value; + return r * (uiControl.max_value - uiControl.min_value) + uiControl.min_value; } return 0; } @@ -214,12 +238,12 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( if (this.imgRef.current) { let img = this.imgRef.current; let rc = img.getBoundingClientRect(); - let x = e.clientX - (rc.left+ rc.width / 2); + let x = e.clientX - (rc.left + rc.width / 2); let y = e.clientY - (rc.top + rc.height / 2); // dead zone in center of image. - if (x*x+y*y < DEAD_ZONE_SIZE*DEAD_ZONE_SIZE) return NaN; + if (x * x + y * y < DEAD_ZONE_SIZE * DEAD_ZONE_SIZE) return NaN; let angle = -Math.atan2(-y, x) * 180 / Math.PI + 90; @@ -229,7 +253,8 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( } captureElement?: SVGSVGElement; pointersDown: number = 0; - + private isTap: boolean = false; + private tapStartMs: number = 0; onPointerDown(e: PointerEvent): void { if (!this.mouseDown && this.isValidPointer(e)) { e.preventDefault(); @@ -238,6 +263,10 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( this.mouseDown = true; + + this.isTap = true; + this.tapStartMs = Date.now(); + this.pointersDown = 1; this.pointerId = e.pointerId; this.pointerType = e.pointerType; @@ -276,12 +305,25 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( } + clickSlop() { + return 3.5; // maybe larger on touch devices. + } onPointerMove(e: PointerEvent): void { if (this.isCapturedPointer(e)) { e.preventDefault(); - this.updateAngle(e) + this.updateAngle(e); + + + let x = e.clientX; + let y = e.clientY; + let dx = x - this.startX; + let dy = y - this.startY; + let distance = Math.sqrt(dx * dx + dy * dy); + if (distance >= this.clickSlop()) { + this.isTap = false; + } } } @@ -297,20 +339,18 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( let angle: number = this.pointerToAngle(e); if (!isNaN(angle)) { if (!isNaN(this.lastPointerAngle)) { - let dAngle = angle-this.lastPointerAngle; + let dAngle = angle - this.lastPointerAngle; while (dAngle > 180) dAngle -= 360; while (dAngle < -180) dAngle += 360; let scale = 1.0; - if (this.pointersDown === 2) - { + if (this.pointersDown === 2) { scale = FINE_RANGE_SCALE; - } else if (this.pointersDown >= 3) - { + } else if (this.pointersDown >= 3) { scale = ULTRA_FINE_RANGE_SCALE; } - this.currentAngle += dAngle*scale; + this.currentAngle += dAngle * scale; let previewValue = this.angleToValue(this.currentAngle); this.previewValue(previewValue); this.props.onPreviewValue(previewValue); @@ -318,21 +358,17 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( this.lastPointerAngle = angle; } } - makeRotationTransform(angle: number): string - { + makeRotationTransform(angle: number): string { return "rotate(" + angle + "deg)"; } - getDefaultRotationTransform(): string - { + getDefaultRotationTransform(): string { let v = this.getCurrentValue(); let a = this.valueToAngle(v); return this.makeRotationTransform(a); } - previewValue(value: number): void - { - if (this.imgRef.current) - { + previewValue(value: number): void { + if (this.imgRef.current) { let img = this.imgRef.current; let angle = this.valueToAngle(value); img.style.transform = this.makeRotationTransform(angle); @@ -363,8 +399,8 @@ const ZoomedDial = withStyles(styles, { withTheme: true })( return ( {this.onDoubleTap();}} onPreviewValue={(v) => { this.setState({ value: v }); }} diff --git a/react/src/svg/file_download_black_24dp.svg b/react/src/svg/file_download_black_24dp.svg index efc0c46..c9c36ed 100644 --- a/react/src/svg/file_download_black_24dp.svg +++ b/react/src/svg/file_download_black_24dp.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/react/src/svg/ic_bank.svg b/react/src/svg/ic_bank.svg new file mode 100644 index 0000000..64a6601 --- /dev/null +++ b/react/src/svg/ic_bank.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/signPackage.sh b/signPackage.sh new file mode 100755 index 0000000..04fef7f --- /dev/null +++ b/signPackage.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#sign a package with rerdavies@gmail.com's private key. +cd build +for filename in *.deb; do + echo gpg --armor --output "$filename".asc -b "$filename" + + gpg --armor --output "$filename".asc -b "$filename" +done +cd .. \ No newline at end of file diff --git a/src/AdminClient.cpp b/src/AdminClient.cpp index 4f77e59..7db67ca 100644 --- a/src/AdminClient.cpp +++ b/src/AdminClient.cpp @@ -41,7 +41,7 @@ AdminClient::~AdminClient() { } -bool AdminClient::CanUseShutdownClient() +bool AdminClient::CanUseAdminClient() { return true; } @@ -104,7 +104,7 @@ bool AdminClient::SetJackServerConfiguration(const JackServerSettings &jackServe void AdminClient::SetWifiConfig(const WifiConfigSettings &settings) { - if (!CanUseShutdownClient()) + if (!CanUseAdminClient()) { throw PiPedalException("Can't perform this operation when debugging."); } @@ -121,7 +121,7 @@ void AdminClient::SetWifiConfig(const WifiConfigSettings &settings) } void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) { - if (!CanUseShutdownClient()) + if (!CanUseAdminClient()) { throw PiPedalException("Can't perform this operation when debugging."); } @@ -139,7 +139,7 @@ void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) void AdminClient::SetGovernorSettings(const std::string &settings) { - if (!CanUseShutdownClient()) + if (!CanUseAdminClient()) { throw PiPedalException("Can't use AdminClient when running interactively."); } @@ -157,7 +157,7 @@ void AdminClient::SetGovernorSettings(const std::string &settings) void AdminClient::MonitorGovernor(const std::string &governor) { - if (!CanUseShutdownClient()) + if (!CanUseAdminClient()) { return; } @@ -174,7 +174,7 @@ void AdminClient::MonitorGovernor(const std::string &governor) } void AdminClient::UnmonitorGovernor() { - if (!CanUseShutdownClient()) + if (!CanUseAdminClient()) { return; } @@ -183,3 +183,16 @@ void AdminClient::UnmonitorGovernor() cmd << '\n'; bool ignored = WriteMessage(cmd.str().c_str()); } + + +void AdminClient::InstallUpdate(const std::string&filename) +{ + if (!CanUseAdminClient()) + { + // generally can't do amin operations when running under a debugger. + throw std::runtime_error("No admin client."); + } + std::stringstream cmd; + cmd << "InstallUpdate " << filename << '\n'; + bool ignroed = WriteMessage(cmd.str().c_str()); +} diff --git a/src/AdminClient.hpp b/src/AdminClient.hpp index 0b59193..5516c90 100644 --- a/src/AdminClient.hpp +++ b/src/AdminClient.hpp @@ -34,7 +34,7 @@ class AdminClient { public: AdminClient(); ~AdminClient(); - bool CanUseShutdownClient(); + bool CanUseAdminClient(); bool RequestShutdown(bool restart); bool SetJackServerConfiguration(const JackServerSettings & jackServerSettings); void SetWifiConfig(const WifiConfigSettings & settings); @@ -42,6 +42,7 @@ public: void SetGovernorSettings(const std::string & governor); void MonitorGovernor(const std::string &governor); void UnmonitorGovernor(); + void InstallUpdate(const std::string&filename); private: std::mutex mutex; UnixSocket socket; diff --git a/src/AdminInstallUpdate.cpp b/src/AdminInstallUpdate.cpp new file mode 100644 index 0000000..ebd6f1e --- /dev/null +++ b/src/AdminInstallUpdate.cpp @@ -0,0 +1,192 @@ +// Copyright (c) 2024Robin 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 "AdminInstallUpdate.hpp" +#include +#include +#include "ss.hpp" +#include +#include +#include +#include "Lv2Log.hpp" +#include +#include +#include +#include +#include "UpdateResults.hpp" +#include "Updater.cpp" + +using namespace pipedal; +namespace fs = std::filesystem; + +static fs::path ROOT_INSTALL_DIRECTORY = "/var/pipedal/updates"; +static fs::path INSTALLER_LOG_FILE_PATH = ROOT_INSTALL_DIRECTORY / "install.log"; + +static fs::path UPDATE_DIRECTORY = ROOT_INSTALL_DIRECTORY / "downloads"; + +static void removeSignalHandler(int signal) +{ + struct sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(signal, &sa, NULL); +} + +static int exec(const std::string &command) +{ + std::string buffer = command; + + + std::vector argv; + char *p = const_cast(buffer.c_str()); + while (*p) + { + char *start = p; + while (*p != ' ' && *p != '\0') + { + ++p; + } + argv.push_back(start); + while (*p == ' ') + { + *p++ = '\0'; + } + } + argv.push_back(nullptr); + + pid_t pid; + + if ((pid = fork()) == 0) + { + execv(argv[0], (char *const *)argv.data()); + write(1,"!\n",2); + exit(EXIT_FAILURE); + } + else + { + + if (pid == -1) + { + write(1,"*",1); + perror("execv"); + return EXIT_FAILURE; + } + int returnValue; + waitpid(pid, &returnValue, 0); + + + int exitStatus = WEXITSTATUS(returnValue); + return exitStatus; + } +} + +void updateLog(const std::string &message) +{ + write(1,message.c_str(),message.length()); + write(1,"\n",1); +} + + +static std::string getVersion(const fs::path path) +{ + std::string fileNameOnly = path.filename(); + std::vector segments = split(path,'_'); + if (segments.size() != 3) return "Unknown"; + return SS("v" << segments[1]); +} +void pipedal::AdminInstallUpdate(const std::filesystem::path path) +{ + + UpdateResults updateResults; + updateResults.updated_ = true; + + updateResults.updateVersion_ = getVersion(path); + try + { + // client needs a disconnect/reconnect even if the update doesn't happen. + + exec("/usr/bin/systemctl stop pipedald"); + + fs:create_directories(ROOT_INSTALL_DIRECTORY); + Lv2Log::info(SS("Installing " << path)); + + if (!path.string().starts_with(UPDATE_DIRECTORY.string())) + { + throw std::runtime_error("Update file path is incorrect."); + } + if (!fs::exists(path)) + { + throw std::runtime_error("File does not exist."); + } + + std::stringstream ssCmd; + ssCmd << "/usr/bin/apt-get -q -y install " << path.string(); + + std::string cmd = ssCmd.str(); + + // direct standard outputs to a log file. + close(0); + close(1); + close(2); + + int fd = open(INSTALLER_LOG_FILE_PATH.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + dup2(fd, 1); // to stdout + dup2(fd, 2); // to stderr + close(fd); + fd = -1; + + // disconnect std input from parent's stdin. + int fdNull = open("/dev/null", O_RDONLY, 0644); + dup2(fdNull, 0); + close(fdNull); + + // verify the signature (again), now that we're running as root and could do real damage + // if we install somebody else's package. + + Updater::ValidateSignature(path, SS(path.string() << ".asc")); // errors are thrown. + + updateLog("** Installing update"); + int retcode = exec(cmd); + + if (retcode != EXIT_SUCCESS) + { + updateResults.updateSuccessful_ = false; + updateResults.updateMessage_ = SS("Update failed. See " << INSTALLER_LOG_FILE_PATH.c_str() << " for further details."); + } else { + updateResults.updateSuccessful_ = true; + updateResults.updateMessage_ = SS("Successfully updated to version " << updateResults.updateVersion_ << "."); + } + + // in case we didn't actually update for some reason. + } + catch (const std::exception &e) + { + updateLog(SS("Error: " << e.what())); + + updateResults.updated_ = false; + updateResults.updateSuccessful_ = false; + updateResults.updateMessage_ = e.what(); + } + updateResults.Save(); + + // restart pipedal to get the client to remove the "Updating..." message. + exec("/usr/bin/systemctl start pipedald"); + +} \ No newline at end of file diff --git a/src/AdminInstallUpdate.hpp b/src/AdminInstallUpdate.hpp new file mode 100644 index 0000000..b2a4699 --- /dev/null +++ b/src/AdminInstallUpdate.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2024Robin 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 + +namespace pipedal +{ + void AdminInstallUpdate(const std::filesystem::path path); + +} \ No newline at end of file diff --git a/src/AdminMain.cpp b/src/AdminMain.cpp index 65b17ce..04d0ca4 100644 --- a/src/AdminMain.cpp +++ b/src/AdminMain.cpp @@ -220,6 +220,12 @@ bool setJackConfiguration(JackServerSettings serverSettings) return true; } +void InstallUpdate(const std::filesystem::path path) +{ + std::string cmd = SS("/usr/bin/systemd-run --unit=pidal_update /usr/sbin/pipedal_update " <driverHost->OnAudioStopped(); + + // if we terminated abnormally, pump messages until we have been terminated. + if (!terminateAudio()) + { + // zero out input buffers. + for (size_t i = 0; i < this->captureBuffers.size(); ++i) + { + float*pBuffer = captureBuffers[i]; + for (size_t j = 0; j < this->bufferSize; ++j) + { + pBuffer[j] = 0; + } + + } + while(!terminateAudio()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + // zero out input buffers. + this->driverHost->OnProcess(this->bufferSize); + + } + } + this->driverHost->OnAudioTerminated(); + + + } bool alsaActive = false; @@ -1947,6 +1973,17 @@ namespace pipedal std::vector &inputAudioPorts, std::vector &outputAudioPorts) { + if (jackServerSettings.IsDummyAudioDevice()) + { + inputAudioPorts.clear(); + inputAudioPorts.push_back("system::capture_0"); + inputAudioPorts.push_back("system::capture_1"); + outputAudioPorts.clear(); + outputAudioPorts.push_back("system::playback_0"); + outputAudioPorts.push_back("system::playback_1"); + + return true; + } snd_pcm_t *playbackHandle = nullptr; snd_pcm_t *captureHandle = nullptr; @@ -1958,7 +1995,7 @@ namespace pipedal try { int err; - for (int retry = 0; retry < 15; ++retry) + for (int retry = 0; retry < 2; ++retry) { err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); if (err < 0) // field report of a device that is present, but won't immediately open. @@ -2044,7 +2081,6 @@ namespace pipedal } catch (const std::exception &e) { - Lv2Log::warning(SS("Unable to read ALSA configuration for " << alsaDeviceName << ". " << e.what() << ".")); result = false; throw; } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 73f3a2d..1d39ad5 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -208,6 +208,9 @@ public: Oscillator oscillator; LatencyMonitor latencyMonitor; + virtual void OnAudioTerminated() { + + } virtual void OnAudioStopped() { } diff --git a/src/AudioConfig.hpp b/src/AudioConfig.hpp index 0acb67c..38ca20b 100644 --- a/src/AudioConfig.hpp +++ b/src/AudioConfig.hpp @@ -24,8 +24,8 @@ #pragma once -#ifndef JACK_HOST -#define JACK_HOST 0 +#ifndef JACK_HOST // jack is no longer supported. +#define JACK_HOST 0 #endif #ifndef ALSA_HOST diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 0442ca2..6d8baae 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -51,6 +51,7 @@ namespace pipedal { virtual void OnProcess(size_t nFrames) = 0; virtual void OnUnderrun() = 0; virtual void OnAudioStopped() = 0; + virtual void OnAudioTerminated() = 0; }; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index d5ef32f..fb742db 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -24,6 +24,7 @@ #include "JackDriver.hpp" #include "AlsaDriver.hpp" +#include "DummyAudioDriver.hpp" #include "AtomConverter.hpp" using namespace pipedal; @@ -49,6 +50,7 @@ using namespace pipedal; #include #include "Lv2EventBufferWriter.hpp" #include "InheritPriorityMutex.hpp" +#include #ifdef __linux__ #include @@ -105,73 +107,88 @@ static std::string GetGovernor() return pipedal::GetCpuGovernor(); } - -class SystemMidiBinding { +class SystemMidiBinding +{ private: MidiBinding currentBinding; bool controlState = false; -public: - void SetBinding(const MidiBinding&binding) { currentBinding = binding; controlState = false; } - bool IsMatch(const MidiEvent&event); - bool IsTriggered(const MidiEvent&event); +public: + void SetBinding(const MidiBinding &binding) + { + currentBinding = binding; + controlState = false; + } + + bool IsMatch(const MidiEvent &event); + bool IsTriggered(const MidiEvent &event); }; bool SystemMidiBinding::IsTriggered(const MidiEvent &event) { switch (currentBinding.bindingType()) { - case BINDING_TYPE_NOTE: - { - if (event.size != 3) return false; - auto command = event.buffer[0] & 0xF0; - if (command != 0x90) return false; // MIDI note on - if (event.buffer[1] != currentBinding.note()) return false; - return event.buffer[2] != 0; // note-on with velicity of zero is a note-off. - } - break; - case BINDING_TYPE_CONTROL: - { - if (event.size != 3) return false; - auto command = event.buffer[0] & 0xF0; - if (command != 0xB0) return false; - if (event.buffer[1] != currentBinding.control()) return false; - bool state = event.buffer[2] >= 0x64; - if (state != this->controlState) - { - this->controlState = state; - return state; - } + case BINDING_TYPE_NOTE: + { + if (event.size != 3) return false; - } - break; - default: + auto command = event.buffer[0] & 0xF0; + if (command != 0x90) + return false; // MIDI note on + if (event.buffer[1] != currentBinding.note()) return false; + return event.buffer[2] != 0; // note-on with velicity of zero is a note-off. + } + break; + case BINDING_TYPE_CONTROL: + { + if (event.size != 3) + return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0xB0) + return false; + if (event.buffer[1] != currentBinding.control()) + return false; + bool state = event.buffer[2] >= 0x64; + if (state != this->controlState) + { + this->controlState = state; + return state; + } + return false; + } + break; + default: + return false; } } -bool SystemMidiBinding::IsMatch(const MidiEvent&event) +bool SystemMidiBinding::IsMatch(const MidiEvent &event) { switch (currentBinding.bindingType()) { - case BINDING_TYPE_NOTE: - { - if (event.size != 3) return false; - auto command = event.buffer[0] & 0xF0; - if (command != 0x80 && command != 0x90) return false; // note on/note off. - return event.buffer[1] == currentBinding.note(); - } - break; - case BINDING_TYPE_CONTROL: - { - if (event.size != 3) return false; - auto command = event.buffer[0] & 0xF0; - if (command != 0xB0) return false; - return event.buffer[1] == currentBinding.control(); - } - break; - default: + case BINDING_TYPE_NOTE: + { + if (event.size != 3) return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0x80 && command != 0x90) + return false; // note on/note off. + return event.buffer[1] == currentBinding.note(); + } + break; + case BINDING_TYPE_CONTROL: + { + if (event.size != 3) + return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0xB0) + return false; + return event.buffer[1] == currentBinding.control(); + } + break; + default: + return false; } } @@ -181,7 +198,6 @@ private: IHost *pHost = nullptr; LV2_Atom_Forge inputWriterForge; - std::mutex atomConverterMutex; AtomConverter atomConverter; @@ -254,7 +270,7 @@ private: Uris uris; - AudioDriver *audioDriver = nullptr; + std::unique_ptr audioDriver; std::recursive_mutex mutex; int64_t overrunGracePeriodSamples = 0; @@ -282,7 +298,9 @@ private: SystemMidiBinding prevMidiBinding; JackChannelSelection channelSelection; - bool active = false; + std::atomic active = false; + std::atomic audioStopped = false; + std::atomic isDummyAudioDriver = false; std::shared_ptr currentPedalboard; std::vector> activePedalboards; // pedalboards that have been sent to the audio queue. @@ -307,7 +325,6 @@ private: return pHost->Lv2UridToString(pAtom->body.otype); } - virtual std::string AtomToJson(const LV2_Atom *pAtom) override { std::lock_guard lock(atomConverterMutex); @@ -315,7 +332,7 @@ private: std::stringstream s; json_writer writer(s); - + writer.write(vAtom); return s.str(); } @@ -328,7 +345,7 @@ private: virtual void Close() { - std::lock_guard guard {mutex}; + std::lock_guard guard{mutex}; if (!isOpen) return; @@ -350,7 +367,6 @@ private: StopReaderThread(); - // release any pdealboards owned by the process thread. this->activePedalboards.resize(0); this->realtimeActivePedalboard = nullptr; @@ -375,7 +391,7 @@ private: delete[] midiLv2Buffers[i]; } midiLv2Buffers.resize(0); - + audioDriver = nullptr; } void ZeroBuffer(float *buffer, size_t nframes) @@ -424,8 +440,7 @@ private: } } - - virtual void SetSystemMidiBindings(const std::vector&bindings); + virtual void SetSystemMidiBindings(const std::vector &bindings); void writeVu() { @@ -633,7 +648,7 @@ private: return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00); } - bool onMidiEvent(Lv2EventBufferWriter&eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent&event) + bool onMidiEvent(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event) { eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); @@ -654,7 +669,6 @@ private: return true; } - void ProcessMidiInput() { Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); @@ -676,7 +690,6 @@ private: Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin(); MidiEvent event; - // write all deferred midi messages. if (deferredMidiMessageCount != 0 && !midiProgramChangePending) { @@ -687,17 +700,16 @@ private: if (deviceIndex == midiDeviceIx) { event.size = messageCount; - event.buffer = deferredMidiMessages +i; + event.buffer = deferredMidiMessages + i; event.time = 0; - onMidiEvent(eventBufferWriter,iterator,event); + onMidiEvent(eventBufferWriter, iterator, event); } i += messageCount; } } - for (size_t frame = 0; frame < n; ++frame) { if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame)) @@ -708,32 +720,32 @@ private: this->deferredMidiMessageCount = 0; // we can discard previous control changes. midiProgramChangePending = true; - this->realtimeWriter.OnMidiProgramChange(++(this->midiProgramChangeId), selectedBank,event.buffer[1]); + this->realtimeWriter.OnMidiProgramChange(++(this->midiProgramChangeId), selectedBank, event.buffer[1]); } else if (isBankChange(event)) { this->selectedBank = event.buffer[2]; - } else if (this->nextMidiBinding.IsMatch(event)) + } + else if (this->nextMidiBinding.IsMatch(event)) { if (nextMidiBinding.IsTriggered(event)) { midiProgramChangePending = true; - this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),1); + this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), 1); } - } else if (this->prevMidiBinding.IsMatch(event)) + } + else if (this->prevMidiBinding.IsMatch(event)) { if (prevMidiBinding.IsTriggered(event)) { midiProgramChangePending = true; - this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),-1); + this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), -1); } } else if (midiProgramChangePending) { // defer the message for processing after the program change has completed. - if (event.size > 0 - && event.size < 128 - && event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE) + if (event.size > 0 && event.size < 128 && event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE) { this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx; this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size; @@ -745,7 +757,7 @@ private: } else { - onMidiEvent(eventBufferWriter,iterator,event); + onMidiEvent(eventBufferWriter, iterator, event); } } } @@ -757,18 +769,24 @@ private: std::mutex audioStoppedMutex; - bool IsAudioActive() + + + bool IsAudioRunning() { - std::lock_guard lock{audioStoppedMutex}; - return this->active; + return this->active && (!this->audioStopped) && !this->isDummyAudioDriver; } - virtual void OnAudioStopped() + virtual void OnAudioStopped() override + { + this->audioStopped = true; + Lv2Log::info("Audio stopped."); + } + virtual void OnAudioTerminated() override { - std::lock_guard lock{audioStoppedMutex}; this->active = false; Lv2Log::info("Audio stopped."); } + virtual void OnProcess(size_t nframes) { try @@ -822,7 +840,7 @@ private: { if (this->realtimeVuBuffers != nullptr) { - pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes,inputBuffers,outputBuffers); + pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes, inputBuffers, outputBuffers); vuSamplesRemaining -= nframes; if (vuSamplesRemaining <= 0) @@ -862,6 +880,7 @@ private: throw; } } + public: AudioHostImpl(IHost *pHost) : inputRingBuffer(RING_BUFFER_SIZE), @@ -875,25 +894,17 @@ public: uris(pHost), atomConverter(pHost->GetMapFeature()) { - realtimeAtomBuffer.resize(32*1024); - lv2_atom_forge_init(&inputWriterForge,pHost->GetMapFeature().GetMap()); + realtimeAtomBuffer.resize(32 * 1024); + lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); -#if JACK_HOST - audioDriver = CreateJackDriver(this); -#endif -#if ALSA_HOST - audioDriver = CreateAlsaDriver(this); -#endif } virtual ~AudioHostImpl() { Close(); CleanRestartThreads(true); - - delete audioDriver; + audioDriver = nullptr; } - virtual JackConfiguration GetServerConfiguration() { JackConfiguration result; @@ -907,8 +918,6 @@ public: return this->sampleRate; } - - void OnAudioComplete() { // there is actually no compelling circumstance in which this should ever happen. @@ -942,7 +951,7 @@ public: { Lv2Log::debug("Service thread priority successfully boosted."); } - SetThreadName("aout"); + SetThreadName("rtsvc"); #else xxx; // TODO! #endif @@ -957,9 +966,9 @@ public: using clock_duration = clock_time::duration; // check for overruns every 30 seconds. - clock_duration waitPeriod = + clock_duration waitPeriod = std::chrono::duration_cast(std::chrono::seconds(30)); - clock_time waitTime = std::chrono::steady_clock::now(); + clock_time waitTime = std::chrono::steady_clock::now(); while (true) { @@ -1022,7 +1031,7 @@ public: RealtimePatchPropertyRequest *pRequest = nullptr; hostReader.read(&pRequest); - std::shared_ptr currentpedalboard; + std::shared_ptr currentpedalboard; { std::lock_guard guard(mutex); @@ -1036,7 +1045,8 @@ public: { if (pRequest->errorMessage == nullptr) { - if (pRequest->GetSize() != 0) { + if (pRequest->GetSize() != 0) + { IEffect *pEffect = currentPedalboard->GetEffect(pRequest->instanceId); if (pEffect == nullptr) { @@ -1044,9 +1054,11 @@ public: } else { - pRequest->jsonResponse = AtomToJson((LV2_Atom*)pRequest->GetBuffer()); + pRequest->jsonResponse = AtomToJson((LV2_Atom *)pRequest->GetBuffer()); } - } else { + } + else + { pRequest->errorMessage = "Plugin did not respond."; } } @@ -1107,13 +1119,13 @@ public: IEffect *pEffect = currentPedalboard->GetEffect(instanceId); if (pEffect != nullptr && this->pNotifyCallbacks) { - LV2_Atom *atom = (LV2_Atom*)&atomBuffer[0]; + LV2_Atom *atom = (LV2_Atom *)&atomBuffer[0]; if (atom->type == uris.atom_Object) { - LV2_Atom_Object *obj = (LV2_Atom_Object*)atom; + LV2_Atom_Object *obj = (LV2_Atom_Object *)atom; if (obj->body.otype == uris.patch_Set) { - + // Get the property and value of the set message const LV2_Atom *property = nullptr; const LV2_Atom *value = nullptr; @@ -1125,8 +1137,8 @@ public: 0); if (property != nullptr && value != nullptr && property->type == uris.atom_URID) { - LV2_URID propertyUrid = ((LV2_Atom_URID*)property)->body; - this->pNotifyCallbacks->OnPatchSetReply(instanceId,propertyUrid,value); + LV2_URID propertyUrid = ((LV2_Atom_URID *)property)->body; + this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value); this->pNotifyCallbacks->OnNotifyMaybeLv2StateChanged(instanceId); } } @@ -1157,31 +1169,34 @@ public: hostReader.read(&body); OnAudioComplete(); return; - } else if (command == RingBufferCommand::MidiProgramChange) + } + else if (command == RingBufferCommand::MidiProgramChange) { RealtimeMidiProgramRequest programRequest; hostReader.read(&programRequest); OnMidiProgramRequest(programRequest); - } else if (command == RingBufferCommand::NextMidiProgram) + } + else if (command == RingBufferCommand::NextMidiProgram) { RealtimeNextMidiProgramRequest request; hostReader.read(&request); pNotifyCallbacks->OnNotifyNextMidiProgram(request); - } else if (command == RingBufferCommand::Lv2ErrorMessage) + } + else if (command == RingBufferCommand::Lv2ErrorMessage) { size_t size; int64_t instanceId; hostReader.read(&instanceId); hostReader.read(&size); - if (this->atomBuffer.size() < size+1) + if (this->atomBuffer.size() < size + 1) { - this->atomBuffer.resize(size+1); + this->atomBuffer.resize(size + 1); } hostReader.read(size, &(atomBuffer[0])); - char *p = (char*)&(atomBuffer[0]); + char *p = (char *)&(atomBuffer[0]); p[size] = 0; std::string message(p); - pNotifyCallbacks->OnNotifyLv2RealtimeError(instanceId,message); + pNotifyCallbacks->OnNotifyLv2RealtimeError(instanceId, message); } else { @@ -1245,6 +1260,22 @@ public: { std::lock_guard guard(mutex); + + if (isOpen) + { + throw PiPedalStateException("Already open."); + } + isOpen = true; + + if (jackServerSettings.IsDummyAudioDevice()) + { + this->isDummyAudioDriver = true; + this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this)); + } else { + this->isDummyAudioDriver = false; + this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); + } + if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) { return; @@ -1253,11 +1284,6 @@ public: this->currentSample = 0; this->underruns = 0; - if (isOpen) - { - throw PiPedalStateException("Already open."); - } - isOpen = true; this->inputRingBuffer.reset(); this->outputRingBuffer.reset(); @@ -1272,7 +1298,6 @@ public: try { - audioDriver->Open(jackServerSettings, this->channelSelection); this->sampleRate = audioDriver->GetSampleRate(); @@ -1287,12 +1312,12 @@ public: } active = true; + audioStopped = false; audioDriver->Activate(); Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription())); } catch (const std::exception &e) { - Lv2Log::error(SS("Failed to start audio. " << e.what())); Close(); active = false; throw; @@ -1368,7 +1393,6 @@ public: } } } - } void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) @@ -1387,22 +1411,22 @@ public: } } - virtual void SetInputVolume(float value) { + virtual void SetInputVolume(float value) + { std::lock_guard guard(mutex); if (active && this->currentPedalboard) { hostWriter.SetInputVolume(value); } - } - virtual void SetOutputVolume(float value) { + virtual void SetOutputVolume(float value) + { std::lock_guard guard(mutex); if (active && this->currentPedalboard) { hostWriter.SetOutputVolume(value); } - } virtual void SetVuSubscriptions(const std::vector &instanceIds) @@ -1436,7 +1460,9 @@ public: vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); - } else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) { + } + else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + { int index = (int)instanceId; VuUpdate v; vuConfig->enabledIndexes.push_back(index); @@ -1444,12 +1470,13 @@ public: if (instanceId == Pedalboard::INPUT_VOLUME_ID) { v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; - } else { - v.isStereoInput_ = v.isStereoOutput_ =this->pHost->GetNumberOfOutputAudioChannels() > 1; + } + else + { + v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1; } vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); - } } @@ -1499,8 +1526,7 @@ public: } private: - - virtual bool UpdatePluginStates(Pedalboard& pedalboard) override; + virtual bool UpdatePluginStates(Pedalboard &pedalboard) override; virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) override; class RestartThread @@ -1575,7 +1601,7 @@ public: } void CleanRestartThreads(bool final) { - std::lock_guard guard {restart_mutex}; + std::lock_guard guard{restart_mutex}; for (size_t i = 0; i < restartThreads.size(); ++i) { if (final) @@ -1641,7 +1667,7 @@ public: result.temperaturemC_ = GetRaspberryPiTemperature(); - result.active_ = IsAudioActive(); + result.active_ = IsAudioRunning(); result.restarting_ = this->restarting; if (this->audioDriver != nullptr) @@ -1680,31 +1706,31 @@ static std::string UriToFieldName(const std::string &uri) return uri.substr(pos + 1); } -void AudioHostImpl::SetSystemMidiBindings(const std::vector&bindings) +void AudioHostImpl::SetSystemMidiBindings(const std::vector &bindings) { for (auto i = bindings.begin(); i != bindings.end(); ++i) { if (i->symbol() == "nextProgram") { this->nextMidiBinding.SetBinding(*i); - } else if (i->symbol() == "prevProgram") + } + else if (i->symbol() == "prevProgram") { this->prevMidiBinding.SetBinding(*i); } } } - AudioHost *AudioHost::CreateInstance(IHost *pHost) { return new AudioHostImpl(pHost); } -bool AudioHostImpl::UpdatePluginStates(Pedalboard& pedalboard) +bool AudioHostImpl::UpdatePluginStates(Pedalboard &pedalboard) { bool changed = false; auto pedalboardItems = pedalboard.GetAllPlugins(); - for (PedalboardItem* item : pedalboardItems) + for (PedalboardItem *item : pedalboardItems) { if (!item->isSplit()) { @@ -1715,14 +1741,14 @@ bool AudioHostImpl::UpdatePluginStates(Pedalboard& pedalboard) } } return true; - } bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) { - IEffect*effect = currentPedalboard->GetEffect(pedalboardItem.instanceId()); + IEffect *effect = currentPedalboard->GetEffect(pedalboardItem.instanceId()); if (effect != nullptr) { - try { + try + { Lv2PluginState state; if (!effect->GetLv2State(&state)) { @@ -1734,7 +1760,8 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) return true; } return false; - } catch(const std::exception &e) + } + catch (const std::exception &e) { Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what())); return false; @@ -1743,9 +1770,6 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) return false; } - - - JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 0c189c2..2455f88 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -166,7 +166,7 @@ public: class JackHostStatus { public: - bool active_; + bool active_ = false; std::string errorMessage_; bool restarting_; uint64_t underruns_; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8b9d091..416e77d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,12 +89,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) set (USE_SANITIZE False) + if (!ENABLE_VST3) - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi") + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi -DARCHITECTURE=${CPACK_SYSTEM_NAME}") else() set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi") endif() +EXECUTE_PROCESS( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) +message(STATUS DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}") + +add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}") if (CMAKE_BUILD_TYPE MATCHES Debug) message(STATUS "Debug build") @@ -132,6 +137,9 @@ else() endif() set (PIPEDAL_SOURCES + UpdateResults.cpp UpdateResults.hpp + UpdaterSecurity.hpp + Updater.cpp Updater.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp @@ -214,6 +222,7 @@ set (PIPEDAL_SOURCES JackDriver.cpp JackDriver.hpp AlsaDriver.cpp AlsaDriver.hpp + DummyAudioDriver.cpp DummyAudioDriver.hpp AudioDriver.hpp AudioConfig.hpp @@ -278,7 +287,8 @@ add_executable(pipedaltest testMain.cpp InvertingMutexTest.cpp jsonTest.cpp - + UpdaterTest.cpp + utilTest.cpp AlsaDriverTest.cpp @@ -563,6 +573,7 @@ add_executable(pipedalconfig CommandLineParser.hpp PiPedalException.hpp ConfigMain.cpp + PiPedalConfiguration.hpp PiPedalConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp SystemConfigFile.hpp SystemConfigFile.cpp WifiChannelSelectors.cpp WifiChannelSelectors.hpp @@ -604,12 +615,22 @@ target_include_directories(capturepresets PRIVATE ${PIPEDAL_INCLUDES}) target_link_libraries(capturepresets ${PIPEDAL_LIBS}) +add_executable(pipedal_update + UpdateMain.cpp + UpdateResults.cpp UpdateResults.hpp + Lv2Log.hpp Lv2Log.cpp + Lv2SystemdLogger.cpp Lv2SystemdLogger.hpp + UpdateResults.cpp UpdateResults.hpp + AdminInstallUpdate.cpp AdminInstallUpdate.hpp +) + +target_link_libraries(pipedal_update PRIVATE stdc++fs systemd PiPedalCommon ) + add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp UnixSocket.cpp UnixSocket.hpp SetWifiConfig.cpp SetWifiConfig.hpp - JackServerSettings.hpp JackServerSettings.cpp Lv2SystemdLogger.hpp Lv2SystemdLogger.cpp @@ -664,7 +685,7 @@ add_custom_target ( install (TARGETS pipedalconfig pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin EXPORT pipedalTargets) - install (TARGETS pipedald pipedaladmind DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin + install (TARGETS pipedald pipedaladmind pipedal_update DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin EXPORT pipedalSbinTargets) diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 34b2e82..6789ed3 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -38,6 +38,7 @@ #include #include "AudioConfig.hpp" #include "WifiChannelSelectors.hpp" +#include "PiPedalConfiguration.hpp" #include #if JACK_HOST @@ -54,6 +55,7 @@ using namespace std; using namespace pipedal; +namespace fs = std::filesystem; #define SERVICE_ACCOUNT_NAME "pipedal_d" #define SERVICE_GROUP_NAME "pipedal_d" @@ -80,12 +82,12 @@ using namespace pipedal; #define REMOVE_OLD_SERVICE 0 // Grandfathering: whether to remove the old shutdown service (now pipedaladmind) #define OLD_SHUTDOWN_SERVICE "pipedalshutdownd" -std::filesystem::path GetServiceFileName(const std::string &serviceName) +fs::path GetServiceFileName(const std::string &serviceName) { - return std::filesystem::path(SERVICE_PATH) / (serviceName + ".service"); + return fs::path(SERVICE_PATH) / (serviceName + ".service"); } -std::filesystem::path findOnPath(const std::string &command) +fs::path findOnPath(const std::string &command) { std::string path = getenv("PATH"); std::vector paths; @@ -98,8 +100,8 @@ std::filesystem::path findOnPath(const std::string &command) pos = path.length(); } std::string thisPath = path.substr(t, pos - t); - std::filesystem::path path = std::filesystem::path(thisPath) / command; - if (std::filesystem::exists(path)) + fs::path path = fs::path(thisPath) / command; + if (fs::exists(path)) { return path; } @@ -112,11 +114,11 @@ std::filesystem::path findOnPath(const std::string &command) void EnableService() { - if (sysExec(SYSTEMCTL_BIN " enable " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) + if (silentSysExec(SYSTEMCTL_BIN " enable " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) { cout << "Error: Failed to enable the " PIPEDALD_SERVICE " service.\n"; } - if (sysExec(SYSTEMCTL_BIN " enable " ADMIN_SERVICE ".service") != EXIT_SUCCESS) + if (silentSysExec(SYSTEMCTL_BIN " enable " ADMIN_SERVICE ".service") != EXIT_SUCCESS) { cout << "Error: Failed to enable the " ADMIN_SERVICE " service.\n"; } @@ -178,8 +180,10 @@ void StopService(bool excludeShutdownService = false) void StartService(bool excludeShutdownService = false) { - - silentSysExec("/usr/bin/pulseaudio --kill"); // interferes with Jack audio service startup. + if (!UsingNetworkManager()) + { + silentSysExec("/usr/bin/pulseaudio --kill"); // interferes with Jack audio service startup. + } if (!excludeShutdownService) { if (sysExec(SYSTEMCTL_BIN " start " ADMIN_SERVICE ".service") != EXIT_SUCCESS) @@ -187,7 +191,7 @@ void StartService(bool excludeShutdownService = false) throw std::runtime_error("Failed to start the " ADMIN_SERVICE " service."); } } - if (sysExec(SYSTEMCTL_BIN " start " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) + if (silentSysExec(SYSTEMCTL_BIN " start " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) { throw std::runtime_error("Failed to start the " PIPEDALD_SERVICE " service."); } @@ -242,7 +246,7 @@ static void RemoveLine(const std::string &path, const std::string lineToRemove) std::vector lines; try { - if (std::filesystem::exists(path)) + if (fs::exists(path)) { { ifstream f(path); @@ -296,10 +300,10 @@ void InstallPamEnv() #if INSTALL_JACK_SERVICE std::string newLine = PAM_LINE; std::vector lines; - std::filesystem::path path = "/etc/security/pam_env.conf"; + fs::path path = "/etc/security/pam_env.conf"; try { - if (std::filesystem::exists(path)) + if (fs::exists(path)) { { ifstream f(path); @@ -353,9 +357,9 @@ void InstallLimits() throw std::runtime_error("Failed to create audio service group."); } - std::filesystem::path limitsConfig = "/etc/security/limits.d/audio.conf"; + fs::path limitsConfig = "/etc/security/limits.d/audio.conf"; - if (!std::filesystem::exists(limitsConfig)) + if (!fs::exists(limitsConfig)) { ofstream output(limitsConfig); output << "# Realtime priority group used by pipedal audio (and also by jack)" @@ -370,9 +374,9 @@ void InstallLimits() #if JACK_HOST void MaybeStartJackService() { - std::filesystem::path drcFile = "/etc/jackdrc"; + fs::path drcFile = "/etc/jackdrc"; - if (std::filesystem::exists(drcFile) && std::filesystem::file_size(drcFile) != 0) + if (fs::exists(drcFile) && fs::file_size(drcFile) != 0) { sysExec(SYSTEMCTL_BIN " start jack"); } @@ -420,9 +424,9 @@ void InstallAudioService() #endif } -int SudoExec(int argc, char**argv) +int SudoExec(int argc, char **argv) { - // re-execute with SUDO in order to prompt for SUDO credentials once only. + // 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())); @@ -464,23 +468,7 @@ void Uninstall() { try { - try - { - if (IsP2pServiceEnabled()) - { - WifiDirectConfigSettings settings; - settings.Load(); - settings.enable_ = false; - settings.Save(); - SetWifiDirectConfig(settings); - } - } - catch (const std::exception e) - { - std::cout << "Error: Unable to disable the Wiifi P2P service. " << e.what() << std::endl; - } - - OnWifiUninstall(); + OnWifiUninstall(true); StopService(); DisableService(); @@ -492,7 +480,7 @@ void Uninstall() try { - std::filesystem::remove("/usr/bin/systemd/system/" OLD_SHUTDOWN_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" OLD_SHUTDOWN_SERVICE ".service"); } catch (...) { @@ -500,21 +488,21 @@ void Uninstall() try { - std::filesystem::remove("/usr/bin/systemd/system/" ADMIN_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" ADMIN_SERVICE ".service"); } catch (...) { } try { - std::filesystem::remove("/usr/bin/systemd/system/" PIPEDAL_NM_P2PD_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" PIPEDAL_NM_P2PD_SERVICE ".service"); } catch (...) { } try { - std::filesystem::remove("/usr/bin/systemd/system/" PIPEDAL_P2PD_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" PIPEDAL_P2PD_SERVICE ".service"); } catch (...) { @@ -522,7 +510,7 @@ void Uninstall() try { - std::filesystem::remove("/usr/bin/systemd/system/" PIPEDALD_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" PIPEDALD_SERVICE ".service"); } catch (...) { @@ -530,7 +518,7 @@ void Uninstall() try { - std::filesystem::remove("/usr/bin/systemd/system/" JACK_SERVICE ".service"); + fs::remove("/usr/bin/systemd/system/" JACK_SERVICE ".service"); } catch (...) { @@ -594,51 +582,75 @@ static std::string RandomChars(int nchars) return s.str(); } +static std::map sPermissionExceptions({{"/var/pipedal/config/NetworkManagerP2P.json", + fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write + +}}); + void SetVarPermissions( - const std::filesystem::path &path, - std::filesystem::perms directoryPermissions, - std::filesystem::perms filePermissions, + const fs::path &path, + fs::perms directoryPermissions, + fs::perms filePermissions, uid_t uid, gid_t gid) { - namespace fs = std::filesystem; using namespace std::filesystem; - try { - if (fs::exists(path)) { - std::ignore = chown(path.c_str(),uid,gid); - if (fs::is_directory(path)) { + try + { + if (fs::exists(path)) + { + std::ignore = chown(path.c_str(), uid, gid); + if (fs::is_directory(path)) + { fs::permissions(path, directoryPermissions, fs::perm_options::replace); - for (const auto& entry : fs::recursive_directory_iterator(path)) { + for (const auto &entry : fs::recursive_directory_iterator(path)) + { - if (chown(entry.path().c_str(),uid,gid) != 0) + if (chown(entry.path().c_str(), uid, gid) != 0) { std::cout << "Error: failed to set ownership of file " << entry.path() << std::endl; } - try { + try + { if (fs::is_directory(entry.path())) { fs::permissions(entry.path(), directoryPermissions, fs::perm_options::replace); - } else { - fs::permissions(entry.path(),filePermissions, fs::perm_options::replace); } - } catch (const std::exception &e) - { - std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; + else + { + fs::permissions(entry.path(), filePermissions, fs::perm_options::replace); + } + } + catch (const std::exception &e) + { + std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; } - } - } else { - fs::permissions(path,filePermissions, fs::perm_options::replace); - } - } else { - std::cout << "Error: Path does not exist: " << path << std::endl; + else + { + if (sPermissionExceptions.contains(path)) + { + fs::permissions(path, sPermissionExceptions[path], fs::perm_options::replace); + } + else + { + fs::permissions(path, filePermissions, fs::perm_options::replace); + } + } } - } catch (const std::filesystem::filesystem_error& e) { + else + { + std::cout << "Error: Path does not exist: " << path << std::endl; + } + } + catch (const fs::filesystem_error &e) + { std::cerr << "Error: " << e.what() << std::endl; - } + } } +namespace fs = std::filesystem; static void FixPermissions() { @@ -647,17 +659,17 @@ static void FixPermissions() fs::create_directories("/var/pipedal"); - fs::perms directoryPermissions = + fs::perms directoryPermissions = fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec | fs::perms::group_read | fs::perms::group_write | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec | fs::perms::set_gid; - - fs::perms filePermissions = - fs::perms::owner_read | fs::perms::owner_write | - fs::perms::group_read | fs::perms::group_write | - fs::perms::others_read ; - + + fs::perms filePermissions = + fs::perms::owner_read | fs::perms::owner_write | + fs::perms::group_read | fs::perms::group_write | + fs::perms::others_read; + uid_t uid; struct passwd *passwd; if ((passwd = getpwnam("pipedal_d")) == nullptr) @@ -667,25 +679,27 @@ static void FixPermissions() } uid = passwd->pw_uid; gid_t gid = passwd->pw_gid; - SetVarPermissions("/var/pipedal",directoryPermissions,filePermissions,uid,gid); + SetVarPermissions("/var/pipedal", directoryPermissions, filePermissions, uid, gid); - fs::perms wpa_supplicant_perms = + fs::perms wpa_supplicant_perms = fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write; - - std::filesystem::path wpa_config_path{ "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"}; - try { - fs::permissions(wpa_config_path,wpa_supplicant_perms, fs::perm_options::replace); - struct group*grp = getgrnam("netdev"); + fs::path wpa_config_path{"/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"}; + try + { + fs::permissions(wpa_config_path, wpa_supplicant_perms, fs::perm_options::replace); + + struct group *grp = getgrnam("netdev"); if (grp != nullptr) { - if (chown(wpa_config_path.c_str(),0,grp->gr_gid) != 0) + if (chown(wpa_config_path.c_str(), 0, grp->gr_gid) != 0) { cout << "Error: Failed to change ownership of " << wpa_config_path << endl; } } - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "Error: Failed to set permissions on " << wpa_config_path << ". " << e.what() << endl; } @@ -717,24 +731,77 @@ static void PrepareServiceConfigurationFile(uint16_t portNumber) void CopyWpaSupplicantConfigFile() { - try { + try + { const char NM_SUPPLICANT_CONFIG_PATH[] = "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"; const char NM_SUPPLICANT_CONFIG_SOURCE_PATH[] = "/etc/pipedal/config/templates/wpa_supplicant-pipedal.conf"; - std::filesystem::path destinationPath = NM_SUPPLICANT_CONFIG_PATH; - std::filesystem::path sourcePath = NM_SUPPLICANT_CONFIG_SOURCE_PATH; - - std::filesystem::create_directories(destinationPath.parent_path()); - std::filesystem::copy_file(sourcePath,destinationPath,std::filesystem::copy_options::overwrite_existing); - } catch (const std::exception&e) + fs::path destinationPath = NM_SUPPLICANT_CONFIG_PATH; + fs::path sourcePath = NM_SUPPLICANT_CONFIG_SOURCE_PATH; + + fs::create_directories(destinationPath.parent_path()); + fs::copy_file(sourcePath, destinationPath, fs::copy_options::overwrite_existing); + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; } } -void Install(const std::filesystem::path &programPrefix, const std::string endpointAddress) + +void DeployVarConfig() +{ + fs::path varConfig = "/var/pipedal/config/config.json"; + if (!fs::exists(varConfig)) + { + auto directory = varConfig.parent_path(); + fs::create_directories(directory); + fs::path templateFile = "/etc/pipedal/config/templates/var_config.json"; + try + { + fs::copy(templateFile, varConfig); + } + catch (const std::exception &e) + { + std::cout << "Error: Failed to create " << varConfig << std::endl; + } + } +} + +void InstallPgpKey() +{ + fs::path homeDir = "/var/pipedal/config/gpg"; + fs::path keyPath = "/etc/pipedal/config/updatekey.gpg"; + + fs::create_directories(homeDir); + std::ignore = chmod(homeDir.c_str(), 0700); + + { + std::stringstream s; + s << (CHOWN_BIN " " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); + sysExec(s.str().c_str()); + } + std::ostringstream ss; + ss << "/usr/bin/gpg --homedir " << homeDir.c_str() << " --import " << keyPath.c_str(); + + int rc = silentSysExec(ss.str().c_str()); + if (rc != EXIT_SUCCESS) + { + cout << "Error: Failed to create update keyring." << endl; + } + + { + std::stringstream ss; + ss << (CHOWN_BIN " -R " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); + std::string cmd = ss.str(); + sysExec(cmd.c_str()); + } +} +void Install(const fs::path &programPrefix, const std::string endpointAddress) { cout << "Configuring pipedal" << endl; try { + DeployVarConfig(); + if (sysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS) { throw std::runtime_error("Failed to create audio service group."); @@ -810,8 +877,8 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo // create and configure /var directory. - std::filesystem::path varDirectory("/var/pipedal"); - std::filesystem::create_directory(varDirectory); + fs::path varDirectory("/var/pipedal"); + fs::create_directory(varDirectory); { std::stringstream s; @@ -839,8 +906,8 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo if (authBindRequired) { - std::filesystem::create_directories("/etc/authbind/byport"); - std::filesystem::path portAuthFile = std::filesystem::path("/etc/authbind/byport") / strPort; + fs::create_directories("/etc/authbind/byport"); + fs::path portAuthFile = fs::path("/etc/authbind/byport") / strPort; { // create it. @@ -928,56 +995,28 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo FixPermissions(); RestartService(false); EnableService(); + + // Restart WiFi Direct if neccessary. + OnWifiReinstall(); + InstallPgpKey(); } catch (const std::exception &e) { // don't allow abnormal termination, which leaves the package in a state that's // difficult to uninstall. cout << "Error: " << e.what(); + cout << " Run 'pipedalconfig --install' again to complete setup of PiPedal." << endl; } } static std::string GetCurrentWebServicePort() { - std::filesystem::path servicePath = GetServiceFileName(PIPEDALD_SERVICE); - try - { - if (std::filesystem::exists(servicePath)) - { - { - ifstream f(servicePath); - if (!f.is_open()) - { - throw std::runtime_error(SS("Can't open " << servicePath)); - } - while (true) - { - std::string line; - if (f.eof()) - break; - std::getline(f, line); - if (line.starts_with("ExecStart=")) - { - auto startPos = line.find("-port "); - if (startPos != std::string::npos) - { - startPos = startPos + 6; - auto endPos = line.find(" -systemd"); - if (endPos != std::string::npos) - { - std::string result = line.substr(startPos, endPos - startPos); - return result; - } - } - } - } - } - } - } - catch (const std::exception & /*ignored*/) - { - } - return ""; + PiPedalConfiguration config; + config.Load("/etc/pipedal/config", ""); + + std::ostringstream ss; + ss << config.GetSocketServerPort(); + return ss.str(); } static void PrintHelp() @@ -1071,7 +1110,6 @@ static void PrintHelp() << HangingIndent() << " --list-p2p-channels [] \tList valid p2p channels for the current/specified country." << "\n\n" - // << HangingIndent() << " --enable-legacy-ap\t \tEnable a legacy Wi-Fi access point." // << "\n\n" // << "Enable a legacy Wi-Fi access point. \n\n" @@ -1122,17 +1160,21 @@ static void PrintHelp() "therefore preferrable under almost all circumstances.\n\n"; } -static int ListP2PChannels(const std::vector&arguments) +static int ListP2PChannels(const std::vector &arguments) { - try { + try + { std::string country; - if (arguments.size() >= 2) + if (arguments.size() >= 2) { throw std::runtime_error("Invalid arguments."); - } if (arguments.size() == 1) + } + if (arguments.size() == 1) { country = arguments[0]; - } else { + } + else + { // use the currently selected country by default. WifiDirectConfigSettings settings; settings.Load(); @@ -1142,18 +1184,18 @@ static int ListP2PChannels(const std::vector&arguments) } country = settings.countryCode_; } - auto channelSelectors = getWifiChannelSelectors(country.c_str(),true); - for (const auto &channelSelector: channelSelectors) + auto channelSelectors = getWifiChannelSelectors(country.c_str(), true); + for (const auto &channelSelector : channelSelectors) { cout << channelSelector.channelName_ << endl; } - } catch (const std::exception &e) + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; return EXIT_FAILURE; } return EXIT_SUCCESS; - } int main(int argc, char **argv) @@ -1187,13 +1229,12 @@ int main(int argc, char **argv) parser.AddOption("--help", &help); parser.AddOption("--prefix", &prefixOption); parser.AddOption("--port", &portOption); - //parser.AddOption("--enable-legacy-ap", &enable_ap); + // parser.AddOption("--enable-legacy-ap", &enable_ap); parser.AddOption("--disable-ap", &disable_ap); parser.AddOption("--enable-p2p", &enable_p2p); parser.AddOption("--disable-p2p", &disable_p2p); parser.AddOption("--list-p2p-channels", &list_p2p_channels); parser.AddOption("--fix-permissions", &fix_permissions); - parser.AddOption("--get-current-port", &get_current_port); // private. For debug use only. @@ -1202,11 +1243,8 @@ int main(int argc, char **argv) { parser.Parse(argc, (const char **)argv); - int actionCount = - help + get_current_port + install + uninstall + stop + start + enable + disable - + enable_ap + disable_ap + restart + enable_p2p + disable_p2p - + list_p2p_channels + fix_permissions - ; + int actionCount = + help + get_current_port + install + uninstall + stop + start + enable + disable + enable_ap + disable_ap + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; if (actionCount > 1) { throw std::runtime_error("Please provide only one action."); @@ -1265,7 +1303,7 @@ int main(int argc, char **argv) auto uid = getuid(); if (uid != 0 && !nosudo) { - return SudoExec(argc,argv); + return SudoExec(argc, argv); } try @@ -1277,17 +1315,17 @@ int main(int argc, char **argv) } if (install) { - std::filesystem::path prefix; + fs::path prefix; if (prefixOption.length() != 0) { - prefix = std::filesystem::path(prefixOption); + prefix = fs::path(prefixOption); } else { - prefix = std::filesystem::path(argv[0]).parent_path().parent_path(); + prefix = fs::path(argv[0]).parent_path().parent_path(); - std::filesystem::path pipedalPath = prefix / "sbin" / "pipedald"; - if (!std::filesystem::exists(pipedalPath)) + fs::path pipedalPath = prefix / "sbin" / "pipedald"; + if (!fs::exists(pipedalPath)) { std::stringstream s; s << "Can't find pipedald executable at " << pipedalPath << ". Try again using the -prefix option."; @@ -1335,7 +1373,8 @@ int main(int argc, char **argv) } else if (enable_p2p) { - try { + try + { auto argv = parser.Arguments(); WifiDirectConfigSettings settings; settings.ParseArguments(argv); @@ -1343,7 +1382,8 @@ int main(int argc, char **argv) settings.enable_ = true; SetWifiDirectConfig(settings); RestartService(true); // also have to retart web service so that it gets the correct device name. - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; return EXIT_FAILURE; diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp new file mode 100644 index 0000000..377aa8d --- /dev/null +++ b/src/DummyAudioDriver.cpp @@ -0,0 +1,564 @@ +/* + * MIT License + * + * 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 "pch.h" +#include "util.hpp" +#include +#include +#include "ss.hpp" +#include "DummyAudioDriver.hpp" +#include "JackServerSettings.hpp" +#include +#include "RtInversionGuard.hpp" +#include "PiPedalException.hpp" +#include +#include +#include + +#include "CpuUse.hpp" + +#include + +#include "Lv2Log.hpp" +#include +#include "ss.hpp" + +#undef ALSADRIVER_CONFIG_DBG + +#ifdef ALSADRIVER_CONFIG_DBG +#include +#endif + +using namespace pipedal; + +namespace pipedal +{ + + [[noreturn]] static void DummyError(const std::string &message) + { + throw PiPedalStateException(message); + } + + + class DummyDriverImpl : public AudioDriver + { + private: + pipedal::CpuUse cpuUse; + + uint32_t sampleRate = 0; + + uint32_t bufferSize; + uint32_t numberOfBuffers; + + int playbackChannels = 2; + int captureChannels = 2; + + + uint32_t playbackSampleSize = 0; + uint32_t captureSampleSize = 0; + uint32_t playbackFrameSize = 0; + uint32_t captureFrameSize = 0; + + + std::vector activeCaptureBuffers; + std::vector activePlaybackBuffers; + + std::vector captureBuffers; + std::vector playbackBuffers; + + uint8_t *rawCaptureBuffer = nullptr; + uint8_t *rawPlaybackBuffer = nullptr; + + AudioDriverHost *driverHost = nullptr; + + public: + DummyDriverImpl(AudioDriverHost *driverHost) + : driverHost(driverHost) + { + } + virtual ~DummyDriverImpl() + { + Close(); + } + + private: + void OnShutdown() + { + Lv2Log::info("Dummy Audio Server has shut down."); + } + + virtual uint32_t GetSampleRate() + { + return this->sampleRate; + } + + JackServerSettings jackServerSettings; + + unsigned int periods = 0; + + + + std::atomic terminateAudio_ = false; + + void terminateAudio(bool terminate) + { + this->terminateAudio_ = terminate; + } + + bool terminateAudio() + { + return this->terminateAudio_; + } + + private: + void DummyCleanup() + { + } + + private: + void AllocateBuffers(std::vector &buffers, size_t n) + { + buffers.resize(n); + for (size_t i = 0; i < n; ++i) + { + buffers[i] = new float[this->bufferSize]; + for (size_t j = 0; j < this->bufferSize; ++j) + { + buffers[i][j] = 0; + } + } + } + + JackChannelSelection channelSelection; + bool open = false; + virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + terminateAudio_ = false; + if (open) + { + throw PiPedalStateException("Already open."); + } + this->jackServerSettings = jackServerSettings; + this->channelSelection = channelSelection; + this->sampleRate = jackServerSettings.GetSampleRate(); + + open = true; + try + { + OpenMidi(jackServerSettings, channelSelection); + OpenAudio(jackServerSettings, channelSelection); + } + catch (const std::exception &e) + { + Close(); + throw; + } + } + virtual std::string GetConfigurationDescription() + { + std::string result = SS( + "DUMMY, " + << "n/a" + << ", " << "Native float" + << ", " << this->sampleRate + << ", " << this->bufferSize << "x" << this->numberOfBuffers + << ", in: " << this->InputBufferCount() << "/" << this->captureChannels + << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + return result; + } + + void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + int err; + + this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); + this->bufferSize = jackServerSettings.GetBufferSize(); + AllocateBuffers(captureBuffers, 2); + AllocateBuffers(playbackBuffers, 2); + + } + + std::jthread *audioThread; + bool audioRunning; + + bool block = false; + + void AudioThread() + { + SetThreadName("dummyAudioDriver"); + try + { +#if defined(__WIN32) + // bump thread prioriy two levels to + // ensure that the service thread doesn't + // get bogged down by UIwork. Doesn't have to be realtime, but it + // MUST run at higher priority than UI threads. + xxx; // TO DO. +#elif defined(__linux__) + int min = sched_get_priority_min(SCHED_RR); + int max = sched_get_priority_max(SCHED_RR); + + struct sched_param param; + memset(¶m, 0, sizeof(param)); + param.sched_priority = RT_THREAD_PRIORITY; + + int result = sched_setscheduler(0, SCHED_RR, ¶m); + if (result == 0) + { + Lv2Log::debug("Service thread priority successfully boosted."); + } + else + { + Lv2Log::error(SS("Failed to set ALSA AudioThread priority. (" << strerror(result) << ")")); + } +#else + xxx; // TODO for your platform. +#endif + + bool ok = true; + + while (true) + { + if (terminateAudio()) + { + break; + } + + ssize_t framesRead = this->bufferSize; + this->driverHost->OnProcess(framesRead); + + /// no attempt at realtime. Just as long as we run occasionally. + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + + } + } + catch (const std::exception &e) + { + Lv2Log::error(e.what()); + Lv2Log::error("Dummy audio thread terminated abnormally."); + } + this->driverHost->OnAudioTerminated(); + } + + bool alsaActive = false; + + static int IndexFromPortName(const std::string &s) + { + auto pos = s.find_last_of('_'); + if (pos == std::string::npos) + { + throw std::invalid_argument("Bad port name."); + } + const char *p = s.c_str() + (pos + 1); + + int v = atoi(p); + if (v < 0) + { + throw std::invalid_argument("Bad port name."); + } + return v; + } + + bool activated = false; + virtual void Activate() + { + if (activated) + { + throw PiPedalStateException("Already activated."); + } + activated = true; + + this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + + playbackBuffers.resize(2); + + int ix = 0; + for (auto &x : channelSelection.GetInputAudioPorts()) + { + int sourceIndex = IndexFromPortName(x); + if (sourceIndex >= captureBuffers.size()) + { + Lv2Log::error(SS("Invalid audio input port: " << x)); + } + else + { + this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; + } + } + + this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); + + ix = 0; + for (auto &x : channelSelection.GetOutputAudioPorts()) + { + int sourceIndex = IndexFromPortName(x); + if (sourceIndex >= playbackBuffers.size()) + { + Lv2Log::error(SS("Invalid audio output port: " << x)); + } + else + { + this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; + } + } + + audioThread = new std::jthread([this]() + { AudioThread(); }); + } + + virtual void Deactivate() + { + if (!activated) + { + return; + } + activated = false; + terminateAudio(true); + if (audioThread) + { + this->audioThread->join(); + this->audioThread = 0; + } + Lv2Log::debug("Audio thread joined."); + } + + static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024; + static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; + + public: + class MidiState + { + private: + snd_rawmidi_t *hIn = nullptr; + snd_rawmidi_params_t *hInParams = nullptr; + std::string deviceName; + + // running status state. + uint8_t runningStatus = 0; + int dataLength = 0; + int dataIndex = 0; + size_t statusBytesRemaining = 0; + size_t data0; + size_t data1; + + size_t eventCount = 0; + MidiEvent events[MAX_MIDI_EVENT]; + size_t bufferCount = 0; + uint8_t buffer[MIDI_BUFFER_SIZE]; + + uint8_t readBuffer[1024]; + + ssize_t sysexStartIndex = -1; + + void checkError(int result, const char *message) + { + if (result < 0) + { + throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName)); + } + } + + public: + void Open(const AlsaMidiDeviceInfo &device) + { + bufferCount = 0; + eventCount = 0; + sysexStartIndex = -1; + runningStatus = 0; + dataIndex = 0; + dataLength = 0; + + this->deviceName = device.description_; + + } + void Close() + { + } + + + size_t GetMidiInputEventCount() + { + return 0; + } + + bool GetMidiInputEvent(MidiEvent *event, size_t nFrame) + { + return false; + } + + void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1) + { + } + + void FillBuffer() + { + } + + void WriteBuffer(uint8_t *readBuffer, size_t nRead) + { + } + }; + + std::vector midiStates; + + void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + const auto &devices = channelSelection.GetInputMidiDevices(); + + midiStates.resize(devices.size()); + + for (size_t i = 0; i < devices.size(); ++i) + { + const auto &device = devices[i]; + MidiState *state = new MidiState(); + midiStates[i] = state; + state->Open(device); + } + } + + virtual size_t MidiInputBufferCount() const + { + return this->midiStates.size(); + } + virtual void *GetMidiInputBuffer(size_t channel, size_t nFrames) + { + return (void *)midiStates[channel]; + } + + virtual size_t GetMidiInputEventCount(void *portBuffer) + { + MidiState *state = (MidiState *)portBuffer; + return state->GetMidiInputEventCount(); + } + virtual bool GetMidiInputEvent(MidiEvent *event, void *portBuf, size_t nFrame) + { + MidiState *state = (MidiState *)portBuf; + return state->GetMidiInputEvent(event, nFrame); + } + + virtual void FillMidiBuffers() + { + } + + virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } + virtual float *GetInputBuffer(size_t channel, size_t nFrames) + { + return activeCaptureBuffers[channel]; + } + + virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } + virtual float *GetOutputBuffer(size_t channel, size_t nFrames) + { + return activePlaybackBuffers[channel]; + } + + void FreeBuffers(std::vector &buffer) + { + for (size_t i = 0; i < buffer.size(); ++i) + { + // delete[] buffer[i]; + buffer[i] = 0; + } + buffer.clear(); + } + void DeleteBuffers() + { + activeCaptureBuffers.clear(); + activePlaybackBuffers.clear(); + FreeBuffers(this->playbackBuffers); + FreeBuffers(this->captureBuffers); + if (rawCaptureBuffer) + { + delete[] rawCaptureBuffer; + rawCaptureBuffer = nullptr; + } + if (rawPlaybackBuffer) + { + delete[] rawPlaybackBuffer; + rawPlaybackBuffer = nullptr; + } + } + virtual void Close() + { + if (!open) + { + return; + } + open = false; + Deactivate(); + DummyCleanup(); + DeleteBuffers(); + } + + virtual float CpuUse() + { + return 0; + } + + virtual float CpuOverhead() + { + return 0.1; + } + }; + + AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost) + { + return new DummyDriverImpl(driverHost); + } + + bool GetDummyChannels(const JackServerSettings &jackServerSettings, + std::vector &inputAudioPorts, + std::vector &outputAudioPorts) + { + + bool result = false; + + try + { + + unsigned int playbackChannels = 2, captureChannels = 2; + + inputAudioPorts.clear(); + for (unsigned int i = 0; i < captureChannels; ++i) + { + inputAudioPorts.push_back(SS("system::capture_" << i)); + } + + outputAudioPorts.clear(); + for (unsigned int i = 0; i < playbackChannels; ++i) + { + outputAudioPorts.push_back(SS("system::playback_" << i)); + } + + result = true; + } + catch (const std::exception &e) + { + throw; + } + return result; + } + +} // namespace diff --git a/src/DummyAudioDriver.hpp b/src/DummyAudioDriver.hpp new file mode 100644 index 0000000..edfcb22 --- /dev/null +++ b/src/DummyAudioDriver.hpp @@ -0,0 +1,35 @@ +/* + * MIT License + * + * 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 "AudioDriver.hpp" +#include "JackServerSettings.hpp" + +namespace pipedal { + + AudioDriver* CreateDummyAudioDriver(AudioDriverHost*driverHost); + +} + diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index 06939ac..11c4129 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -112,7 +112,12 @@ void JackConfiguration::AlsaInitialize( this->isValid_ = false; this->errorStatus_ = ""; - this->inputMidiDevices_ = GetAlsaMidiInputDevices(); + if (jackServerSettings.IsDummyAudioDevice()) + { + this->inputMidiDevices_.clear(); + } else { + this->inputMidiDevices_ = GetAlsaMidiInputDevices(); + } if (jackServerSettings.IsValid()) { this->blockLength_ = jackServerSettings.GetBufferSize(); @@ -126,7 +131,7 @@ void JackConfiguration::AlsaInitialize( } } catch (const std::exception& /*ignore*/) { - + throw; } } diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index 6d27282..1477a7b 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -54,6 +54,14 @@ namespace pipedal uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; } const std::string &GetAlsaInputDevice() const { return alsaDevice_; } + void UseDummyAudioDevice() { + this->valid_ = true; + if (sampleRate_ == 0) sampleRate_ = 48000; + this->alsaDevice_ = "__DUMMY_AUDIO__"; + } + bool IsDummyAudioDevice() const { + return this->alsaDevice_ == "__DUMMY_AUDIO__"; + } void ReadJackDaemonConfiguration(); diff --git a/src/Lv2PluginChangeMonitor.cpp b/src/Lv2PluginChangeMonitor.cpp index 291c53a..b4b57f3 100644 --- a/src/Lv2PluginChangeMonitor.cpp +++ b/src/Lv2PluginChangeMonitor.cpp @@ -27,6 +27,7 @@ #include #include #include "PiPedalModel.hpp" +#include "util.hpp" using namespace pipedal; @@ -42,8 +43,11 @@ void Lv2PluginChangeMonitor::Shutdown() if (monitorThread) { terminateThread = true; + uint64_t val = 1; + write(shutdown_eventfd,(void*)&val, sizeof(val)); monitorThread->join(); monitorThread = nullptr; + close(shutdown_eventfd); } } @@ -54,6 +58,7 @@ Lv2PluginChangeMonitor::~Lv2PluginChangeMonitor() void Lv2PluginChangeMonitor::ThreadProc() { + SetThreadName("Lv2Change"); using clock = std::chrono::steady_clock; int inotify_fd = inotify_init(); diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index 0a5b658..58094ed 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -345,6 +345,10 @@ public: virtual void OnAudioStopped() { + } + virtual void OnAudioTerminated() + { + } virtual void OnProcess(size_t nFrames) { diff --git a/src/PiPedalConfiguration.cpp b/src/PiPedalConfiguration.cpp index 5fec21b..f333d95 100644 --- a/src/PiPedalConfiguration.cpp +++ b/src/PiPedalConfiguration.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2024 Robin 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 @@ -20,9 +20,76 @@ #include "pch.h" #include "PiPedalConfiguration.hpp" #include "json.hpp" - +#include "ss.hpp" +#include "ServiceConfiguration.hpp" using namespace pipedal; +namespace fs = std::filesystem; + +void PiPedalConfiguration::Load(const std::filesystem::path &path, const std::filesystem::path &webRoot) +{ + docRoot_ = path; + webRoot_ = webRoot; + + // load installer defaults. + { + std::filesystem::path configPath = path / "config.json"; + std::ifstream f(configPath); + if (!f.is_open()) + { + std::stringstream s; + s << "Unable to open " << configPath; + throw PiPedalStateException(s.str()); + } + json_reader reader(f); + reader.read(this); + } + // web port comes from service.conf + ServiceConfiguration serviceConfiguration; + + serviceConfiguration.Load(fs::path(this->local_storage_path_) / "config" / "service.conf"); + this->socketServerAddress_ = SS("0.0.0.0:" << serviceConfiguration.server_port); + + // load any user-made settings + { + std::filesystem::path userPath = std::filesystem::path(this->local_storage_path_) / "config" / "config.json"; + if (std::filesystem::exists(userPath)) + { + std::ifstream f(userPath); + json_reader reader(f); + reader.read(this); + } + } +} + +uint16_t PiPedalConfiguration::GetSocketServerPort() const +{ + try + { + size_t pos = this->socketServerAddress_.find_last_of(':'); + if (pos == std::string::npos) + { + throw std::exception(); + } + std::string strPort = socketServerAddress_.substr(pos + 1); + unsigned long port = std::stoul(strPort); + if (port == 0) + { + throw std::exception(); + } + if (port > std::numeric_limits::max()) + { + throw std::exception(); + } + return (uint16_t)port; + } + catch (const std::exception &) + { + std::stringstream s; + s << "Invalid port number in '" << this->socketServerAddress_ << "'."; + throw PiPedalArgumentException(s.str()); + } +} JSON_MAP_BEGIN(PiPedalConfiguration) JSON_MAP_REFERENCE(PiPedalConfiguration, lv2_path) @@ -37,4 +104,5 @@ JSON_MAP_REFERENCE(PiPedalConfiguration, maxUploadSize) JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointGateway) JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointServerAddress) JSON_MAP_REFERENCE(PiPedalConfiguration, isVst3Enabled) +JSON_MAP_REFERENCE(PiPedalConfiguration, end) JSON_MAP_END() diff --git a/src/PiPedalConfiguration.hpp b/src/PiPedalConfiguration.hpp index c2fe24e..282c9f8 100644 --- a/src/PiPedalConfiguration.hpp +++ b/src/PiPedalConfiguration.hpp @@ -48,6 +48,7 @@ private: std::string accessPointGateway_; std::string accessPointServerAddress_; bool isVst3Enabled_ = true; + bool end_ = false; // dummy target for /var/pipedal/config/config.json public: bool IsVst3Enabled() const { return isVst3Enabled_; } @@ -57,26 +58,8 @@ public: const std::filesystem::path& GetWebRoot() const { return webRoot_; } - void Load(const std::filesystem::path& path, const std::filesystem::path&webRoot) { - std::filesystem::path configPath = path / "config.json"; - if (!std::filesystem::exists(configPath)) - { - throw PiPedalException("File not found."); - } - std::ifstream f(configPath); - if (!f.is_open()) - { - std::stringstream s; - s << "Unable to open " << configPath; - throw PiPedalStateException(s.str()); - } - json_reader reader(f); - reader.read(this); - docRoot_ = path; + void Load(const std::filesystem::path& path, const std::filesystem::path&webRoot); - webRoot_ = webRoot; - - } const std::filesystem::path &GetDocRoot() const { return docRoot_; } const std::string &GetLv2Path() const { return lv2_path_; } const std::string &GetLocalStoragePath() const { return local_storage_path_; } @@ -108,32 +91,8 @@ public: return socketServerAddress_.substr(0,pos); } - uint16_t GetSocketServerPort() const { - try { - size_t pos = this->socketServerAddress_.find_last_of(':'); - if (pos == std::string::npos) - { - throw std::exception(); - } - std::string strPort = socketServerAddress_.substr(pos+1); - unsigned long port = std::stoul(strPort); - if (port == 0) - { - throw std::exception(); - } - if (port > std::numeric_limits::max()) - { - throw std::exception(); - } - return (uint16_t) port; - } catch (const std::exception &) - { - std::stringstream s; - s << "Invalid port number in '" << this->socketServerAddress_ << "'."; - throw PiPedalArgumentException(s.str()); - } + uint16_t GetSocketServerPort() const; - } uint32_t GetThreads() const { return threads_; } DECLARE_JSON_MAP(PiPedalConfiguration); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 5972714..92eadcc 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -66,6 +66,7 @@ PiPedalModel::PiPedalModel() : pluginHost(), atomConverter(pluginHost.GetMapFeature()) { + this->currentUpdateStatus = updater.GetCurrentStatus(); this->pedalboard = Pedalboard::MakeDefault(); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. @@ -73,6 +74,11 @@ PiPedalModel::PiPedalModel() #else this->jackServerSettings = this->storage.GetJackServerSettings(); #endif + updater.SetUpdateListener( + [this](const UpdateStatus &updateStatus) + { + this->OnUpdateStatusChanged(updateStatus); + }); } void PiPedalModel::Close() @@ -242,64 +248,8 @@ void PiPedalModel::Load() scheduler_params.sched_priority = 10; memset(&scheduler_params, 0, sizeof(sched_param)); sched_setscheduler(0, SCHED_RR, &scheduler_params); -#if JACK_HOST - this->jackConfiguration = this->jackConfiguration.JackInitialize(); -#else - this->jackServerSettings = storage.GetJackServerSettings(); - if (jackServerSettings.IsValid()) - { - this->jackConfiguration.AlsaInitialize(this->jackServerSettings); - if (!jackServerSettings.IsValid()) - { - for (size_t retry = 0; retry < 5; ++retry) - { - // retry until the device comes online. - sleep(3); - Lv2Log::info("Retrying AlsaInitialize"); - jackConfiguration.isValid(true); - this->jackConfiguration.AlsaInitialize(this->jackServerSettings); - if (jackConfiguration.isValid()) - { - Lv2Log::info("Retry succeeded."); - break; - } - } - } - } - else - { - this->jackConfiguration.AlsaInitialize(this->jackServerSettings); - } -#endif - if (this->jackConfiguration.isValid()) - { - JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); - selection = selection.RemoveInvalidChannels(jackConfiguration); - this->pluginHost.OnConfigurationChanged(jackConfiguration, selection); - try - { - audioHost->Open(this->jackServerSettings, selection); - bool loadedSuccessfully = false; - try - { - LoadCurrentPedalboard(); - loadedSuccessfully = true; - } - catch (const std::exception &e) - { - Lv2Log::error("Failed to load initial plugin. %s", e.what()); - } - } - catch (std::exception &e) - { - Lv2Log::error("Failed to start audio device. %s", e.what()); - } - } - else - { - Lv2Log::error("Audio device not configured."); - } + RestartAudio(); } IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId) @@ -386,9 +336,8 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) item->stateUpdateCount(item->stateUpdateCount() + 1); - IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; - Lv2PluginState newState = item->lv2State(); + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; for (size_t i = 0; i < subscribers.size(); ++i) { t[i] = this->subscribers[i]; @@ -1138,59 +1087,65 @@ JackConfiguration PiPedalModel::GetJackConfiguration() return this->jackConfiguration; } -void PiPedalModel::RestartAudio() +void PiPedalModel::RestartAudio(bool useDummyAudioDriver) { - if (this->audioHost->IsOpen()) - { - - this->audioHost->Close(); - } - // restarting is a bit dodgy. It was impossible with Jack, but - // now very plausible with the ALSA audio stack. - - // Still bugs wrt/ restarting the circular buffers for the audio thread. - - // for the meantime, just rely on the fact that the admin service will restart - // the process. - //... - - // do a complete reload. - - this->audioHost->SetPedalboard(nullptr); - - this->jackConfiguration.AlsaInitialize(this->jackServerSettings); - - if (this->jackConfiguration.isValid()) - { - JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); - selection = selection.RemoveInvalidChannels(jackConfiguration); - } - else - { - jackConfiguration.setErrorStatus("Error"); - } - - FireJackConfigurationChanged(jackConfiguration); - - if (!jackServerSettings.IsValid() || !jackConfiguration.isValid()) - { - Lv2Log::error("Audio configuration not valid."); - return; - } try { - JackChannelSelection channelSelection = GetJackChannelSelection(); + if (this->audioHost->IsOpen()) + { + + this->audioHost->Close(); + } + // restarting is a bit dodgy. It was impossible with Jack, but + // now very plausible with the ALSA audio stack. + + // Still bugs wrt/ restarting the circular buffers for the audio thread. + + // do a complete reload. + + this->audioHost->SetPedalboard(nullptr); + + auto jackServerSettings = this->jackServerSettings; + if (useDummyAudioDriver) + { + jackServerSettings.UseDummyAudioDevice(); + } + + auto jackConfiguration = this->jackConfiguration; + jackConfiguration.AlsaInitialize(jackServerSettings); + if (jackConfiguration.isValid()) + { + JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); + selection = selection.RemoveInvalidChannels(jackConfiguration); + } + else + { + jackConfiguration.setErrorStatus("Error"); + } + if (!useDummyAudioDriver) + { + this->jackConfiguration = jackConfiguration; + FireJackConfigurationChanged(jackConfiguration); + } + + if (!jackServerSettings.IsValid() || !jackConfiguration.isValid()) + { + throw std::runtime_error("Audio configuration not valid."); + } + + JackChannelSelection channelSelection = this->storage.GetJackChannelSelection(jackConfiguration); + if (this->jackConfiguration.isValid()) + { + channelSelection.RemoveInvalidChannels(this->jackConfiguration); + } if (!channelSelection.isValid()) { - Lv2Log::error("Audio configuration not valid."); - return; + throw std::runtime_error("Audio configuration not valid."); } - this->audioHost->Open(this->jackServerSettings, channelSelection); + this->audioHost->Open(jackServerSettings, channelSelection); this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - std::vector errorMessages; - LoadCurrentPedalboard(); this->UpdateRealtimeVuSubscriptions(); @@ -1198,8 +1153,19 @@ void PiPedalModel::RestartAudio() } catch (const std::exception &e) { - Lv2Log::error(SS("Failed to start audio. " << e.what())); - throw; + this->audioHost->Close(); + if (useDummyAudioDriver) + { + Lv2Log::error(SS("Failed to start dummy audio driver. " << e.what())); + } + else + { + Lv2Log::error(SS("Failed to start audio. " << e.what())); + } + if (!useDummyAudioDriver) + { + RestartAudio(true); // use the dummy audio driver. + } } } @@ -1293,7 +1259,6 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f { if (portIndex == -1) { - // bypass! yyy this->value!!! this->pedalboard.SetItemEnabled(instanceId, value != 0); // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; @@ -2172,3 +2137,110 @@ void PiPedalModel::SetRestartListener(std::function &&listener) { this->restartListener = std::move(listener); } + +void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus &updateStatus) +{ + std::lock_guard lock(mutex); + + if (this->currentUpdateStatus != updateStatus) + { + this->currentUpdateStatus = updateStatus; + FireUpdateStatusChanged(this->currentUpdateStatus); + } +} +void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus) +{ + std::lock_guard lock(mutex); + + std::vector t; + t.reserve(subscribers.size()); + + for (auto subscriber : subscribers) + { + t.push_back(subscriber); + } + + for (auto subscriber : t) + { + subscriber->OnUpdateStatusChanged(updateStatus); + } +} +UpdateStatus PiPedalModel::GetUpdateStatus() +{ + std::lock_guard lock(mutex); + return updater.GetCurrentStatus(); +} + +void PiPedalModel::UpdateNow(const std::string &updateUrl) +{ + std::lock_guard lock(mutex); + std::filesystem::path fileName,signatureName; + updater.DownloadUpdate(updateUrl,&fileName,&signatureName); + + adminClient.InstallUpdate(fileName); +} +void PiPedalModel::ForceUpdateCheck() +{ + updater.ForceUpdateCheck(); +} +void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy) +{ + updater.SetUpdatePolicy(updatePolicy); +} + +static bool HasAlsaDevice(const std::vector devices, const std::string &deviceId) +{ + for (auto &device : devices) + { + if (device.id_ == deviceId) + return true; + } + return false; +} + +void PiPedalModel::WaitForAudioDeviceToComeOnline() +{ + auto serverSettings = this->GetJackServerSettings(); + // Wait for selected audio device to be initialized. + // It may take some time for ALSA to publish all available devices when rebooting. + + if (serverSettings.IsValid()) + { + // wait up to 15 seconds for the midi device to come online. + auto devices = GetAlsaDevices(); + bool found = false; + if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice())) + { + Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); + } + else + { + Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); + for (int i = 0; i < 5; ++i) + { + sleep(2); + devices = GetAlsaDevices(); + if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice())) + { + found = true; + break; + } + } + if (found) + { + Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); + } + else + { + Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found.")); + } + } + } + else + { + Lv2Log::info("No ALSA device selected."); + } + + // pre-cache device info before we let audio services run. + GetAlsaDevices(); +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index a9aa49b..06c97b1 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -30,6 +30,7 @@ #include #include #include "Banks.hpp" +#include "Updater.hpp" #include "PiPedalConfiguration.hpp" #include "JackServerSettings.hpp" #include "WifiConfigSettings.hpp" @@ -56,7 +57,7 @@ namespace pipedal virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0; virtual void OnInputVolumeChanged(float value) = 0; virtual void OnOutputVolumeChanged(float value) = 0; - + virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) = 0; virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0; virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0; virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0; @@ -86,6 +87,10 @@ namespace pipedal class PiPedalModel : private IAudioHostCallbacks { private: + + UpdateStatus currentUpdateStatus; + Updater updater; + void OnUpdateStatusChanged(const UpdateStatus&updateStatus); std::function restartListener; std::unique_ptr pluginChangeMonitor; @@ -164,9 +169,8 @@ namespace pipedal void UpdateRealtimeVuSubscriptions(); void UpdateRealtimeMonitorPortSubscriptions(); - void OnVuUpdate(const std::vector &updates); - void RestartAudio(); + void RestartAudio(bool useDummyAudioDriver = false); std::vector outstandingParameterRequests; @@ -199,6 +203,11 @@ namespace pipedal PiPedalModel(); virtual ~PiPedalModel(); + void WaitForAudioDeviceToComeOnline(); + + UpdateStatus GetUpdateStatus(); + void UpdateNow(const std::string&updateUrl); + void FireUpdateStatusChanged(const UpdateStatus&updateStatus); uint16_t GetWebPort() const { return webPort; } std::filesystem::path GetPluginUploadDirectory() const; void Close(); @@ -334,7 +343,8 @@ namespace pipedal std::map GetFavorites() const; void SetFavorites(const std::map &favorites); - + void SetUpdatePolicy(UpdatePolicyT updatePolicy); + void ForceUpdateCheck(); std::vector GetFileList(const UiFileProperty&fileProperty); std::vector GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index ac5b6b7..bfdd22a 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -20,6 +20,7 @@ #include "pch.h" #include "PiPedalSocket.hpp" +#include "Updater.hpp" #include "json.hpp" #include "viewstream.hpp" #include "PiPedalVersion.hpp" @@ -1001,7 +1002,19 @@ public: pReader->read(&handle); this->model.CancelMonitorPatchProperty(this->clientId, handle); } - + else if (message == "getUpdateStatus") + { + UpdateStatus updateStatus = model.GetUpdateStatus(); + this->Reply(replyTo,"getUpdateStatus",updateStatus); + } + else if (message == "updateNow") + { + std::string updateUrl; + pReader->read(&updateUrl); + model.UpdateNow(updateUrl); + bool result = true; + this->Reply(replyTo,"updateNow",result); + } else if (message == "getJackStatus") { JackHostStatus status = model.GetJackStatus(); @@ -1054,7 +1067,7 @@ public: { WifiConfigSettings wifiConfigSettings; pReader->read(&wifiConfigSettings); - if (!GetAdminClient().CanUseShutdownClient()) + if (!GetAdminClient().CanUseAdminClient()) { throw PiPedalException("Can't change server settings when running interactively."); } @@ -1075,7 +1088,7 @@ public: { WifiDirectConfigSettings wifiDirectConfigSettings; pReader->read(&wifiDirectConfigSettings); - if (!GetAdminClient().CanUseShutdownClient()) + if (!GetAdminClient().CanUseAdminClient()) { throw PiPedalException("Can't change server settings when running interactively."); } @@ -1442,6 +1455,18 @@ public: pReader->read(&favorites); this->model.SetFavorites(favorites); } + else if (message == "setUpdatePolicy") + { + int iPolicy; + pReader->read(&iPolicy); + + this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy); + } + else if (message == "forceUpdateCheck") + { + this->model.ForceUpdateCheck(); + + } else if (message == "setSystemMidiBindings") { std::vector bindings; @@ -1581,6 +1606,10 @@ protected: } private: + virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) { + Send("onUpdateStatusChanged",updateStatus); + } + virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state) { Lv2StateChangedBody message { (uint64_t)instanceId, state}; @@ -1949,7 +1978,7 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel & void PiPedalSocketHandler::RequestShutdown(bool restart) { - if (GetAdminClient().CanUseShutdownClient()) + if (GetAdminClient().CanUseAdminClient()) { GetAdminClient().RequestShutdown(restart); } diff --git a/src/SetWifiConfig.cpp b/src/SetWifiConfig.cpp index 565f825..13b0f0a 100644 --- a/src/SetWifiConfig.cpp +++ b/src/SetWifiConfig.cpp @@ -549,13 +549,18 @@ void UninstallP2p() restoreP2pDnsmasqConfFile(); restoreNetworkManagerP2pConfig(); - sysExec(SYSTEMCTL_BIN " restart dhcpcd"); + if (!UsingNetworkManager()) + { + sysExec(SYSTEMCTL_BIN " restart dhcpcd"); + } else { + sysExec(SYSTEMCTL_BIN " restart NetworkManager"); // bring up wlan0 wifi. + + } WifiDirectConfigSettings wifiDirectConfigSettings; wifiDirectConfigSettings.Load(); wifiDirectConfigSettings.enable_ = false; - wifiDirectConfigSettings.Save(); - sysExec(SYSTEMCTL_BIN " restart NetworkManager"); // bring up wlan0 wifi. + SetWifiDirectConfig(wifiDirectConfigSettings); } } @@ -675,8 +680,15 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) cout << e.what() << endl; } } - -void pipedal::OnWifiUninstall() +void pipedal::OnWifiReinstall() { + WifiDirectConfigSettings settings; + settings.Load(); + if (settings.enable_) + { + SetWifiDirectConfig(settings); + } +} +void pipedal::OnWifiUninstall(bool preserveState) { // intaller hook if (IsApdInstalled()) @@ -685,7 +697,15 @@ void pipedal::OnWifiUninstall() } if (IsP2pInstalled()) { + WifiDirectConfigSettings settings; + settings.Load(); UninstallP2p(); + + // preserve the state for future installs. + if (preserveState) + { + settings.Save(); + } } } void pipedal::OnWifiInstallComplete() diff --git a/src/SetWifiConfig.hpp b/src/SetWifiConfig.hpp index e902b60..820b94b 100644 --- a/src/SetWifiConfig.hpp +++ b/src/SetWifiConfig.hpp @@ -29,7 +29,9 @@ namespace pipedal { void SetWifiConfig(const WifiConfigSettings&settings); void SetWifiDirectConfig(const WifiDirectConfigSettings&settings); - void OnWifiUninstall(); + void OnWifiUninstall(bool preserveState = false); + void OnWifiReinstall(); + void OnWifiInstallComplete(); bool UsingNetworkManager(); diff --git a/src/UpdateMain.cpp b/src/UpdateMain.cpp new file mode 100644 index 0000000..c0bb4ab --- /dev/null +++ b/src/UpdateMain.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2024Robin 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 "AdminInstallUpdate.hpp" +#include "Lv2Log.hpp" +#include "Lv2SystemdLogger.hpp" +#include + +using namespace pipedal; + +int main(int argc, char**argv) +{ + Lv2Log::set_logger(MakeLv2SystemdLogger()); + + if (argc != 2) + { + Lv2Log::error("Invalid arguments."); + return EXIT_FAILURE; + } + std::filesystem::path path = argv[1]; + AdminInstallUpdate(path); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/UpdateResults.cpp b/src/UpdateResults.cpp new file mode 100644 index 0000000..1bc8b6a --- /dev/null +++ b/src/UpdateResults.cpp @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Robin 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 "UpdateResults.hpp" +#include +#include +#include "Lv2Log.hpp" +#include "ss.hpp" + + + +using namespace pipedal; +namespace fs = std::filesystem; + +const fs::path UPDATE_RESULT_PATH{"/var/pipedal/config"}; +void UpdateResults::Load() +{ + std::ifstream f(UPDATE_RESULT_PATH); + if (f.is_open()) + { + json_reader reader(f); + reader.read(this); + } +} +void UpdateResults::Save() +{ + std::ofstream f {UPDATE_RESULT_PATH}; + if (f.is_open()) + { + json_writer writer(f); + writer.write(this); + } else { + Lv2Log::error(SS("Can't write to " << UPDATE_RESULT_PATH)); + } +} + + +JSON_MAP_BEGIN(UpdateResults) +JSON_MAP_REFERENCE(UpdateResults, updated) +JSON_MAP_REFERENCE(UpdateResults, updateSuccessful) +JSON_MAP_REFERENCE(UpdateResults, updateVersion) +JSON_MAP_REFERENCE(UpdateResults, updateMessage) +JSON_MAP_REFERENCE(UpdateResults, installerLog) +JSON_MAP_END() diff --git a/src/UpdateResults.hpp b/src/UpdateResults.hpp new file mode 100644 index 0000000..207f023 --- /dev/null +++ b/src/UpdateResults.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Robin 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 "json.hpp" + +namespace pipedal { + class UpdateResults { + public: + bool updated_ = false; + bool updateSuccessful_ = false; + std::string updateVersion_; + std::string updateMessage_; + std::string installerLog_; + + void Load(); + void Save(); + + DECLARE_JSON_MAP(UpdateResults); + }; + +} \ No newline at end of file diff --git a/src/Updater.cpp b/src/Updater.cpp new file mode 100644 index 0000000..4357c83 --- /dev/null +++ b/src/Updater.cpp @@ -0,0 +1,999 @@ +// Copyright (c) 2024 Robin 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 "Updater.hpp" +#include "util.hpp" +#include "json.hpp" +#include "config.hpp" +#include +#include +#include +#include +#include +#include "Lv2Log.hpp" +#include "SysExec.hpp" +#include "json_variant.hpp" +#include "ss.hpp" +#include "Lv2Log.hpp" +#include +#include "UpdaterSecurity.hpp" +#include "SysExec.hpp" + +using namespace pipedal; +namespace fs = std::filesystem; + +#undef TEST_UPDATE + +#ifndef DEBUG +#undef TEST_UPDATE // do NOT leat this leak into a production build! +#endif + +static constexpr uint64_t CLOSE_EVENT = 0; +static constexpr uint64_t CHECK_NOW_EVENT = 1; +static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2; + +static std::filesystem::path WORKING_DIRECTORY = "/var/pipedal/updates"; +static std::filesystem::path UPDATE_STATUS_CACHE_FILE = WORKING_DIRECTORY / "updateStatus.json"; + +Updater::clock::duration Updater::updateRate = std::chrono::duration_cast(std::chrono::days(1)); +static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast(std::chrono::minutes(30)); + +std::mutex cacheMutex; +static UpdateStatus GetCachedUpdateStatus() +{ + std::lock_guard lock{cacheMutex}; + try + { + if (std::filesystem::exists(UPDATE_STATUS_CACHE_FILE)) + { + std::ifstream f{UPDATE_STATUS_CACHE_FILE}; + if (f.is_open()) + { + json_reader reader(f); + UpdateStatus status; + reader.read(&status); + + // cached curruent version might come from a different version. + status.ResetCurrentVersion(); + return status; + } + } + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Unable to read cached UpdateStatus. " << e.what())); + } + return UpdateStatus(); +} + +static void SetCachedUpdateStatus(UpdateStatus &updateStatus) +{ + std::lock_guard lock{cacheMutex}; + updateStatus.LastUpdateTime(std::chrono::system_clock::now()); + try + { + std::ofstream f{UPDATE_STATUS_CACHE_FILE}; + json_writer writer{f}; + writer.write(updateStatus); + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what())); + } +} +Updater::Updater() +{ + cachedUpdateStatus = GetCachedUpdateStatus(); + this->updatePolicy = cachedUpdateStatus.UpdatePolicy(); + currentResult = cachedUpdateStatus; + + int fds[2]; + int rc = pipe(fds); + if (rc != 0) + { + throw std::runtime_error("Updater: cant create event pipe."); + } + this->event_reader = fds[0]; + this->event_writer = fds[1]; + + this->thread = std::make_unique([this]() + { ThreadProc(); }); + CheckNow(); +} +Updater::~Updater() +{ + Stop(); +} +void Updater::Stop() +{ + if (stopped) + { + return; + } + stopped = true; + + if (event_writer != -1) + { + uint64_t value = CLOSE_EVENT; + write(this->event_writer, &value, sizeof(uint64_t)); + } + if (thread) + { + thread->join(); + thread = nullptr; + } + if (event_reader != -1) + { + close(event_reader); + } + if (event_writer != -1) + { + close(event_writer); + } +} + +void Updater::CheckNow() +{ + uint64_t value = CHECK_NOW_EVENT; + write(this->event_writer, &value, sizeof(uint64_t)); +} + +void Updater::SetUpdateListener(UpdateListener &&listener) +{ + std::lock_guard lock{mutex}; + this->listener = listener; + if (hasInfo) + { + listener(currentResult); + } +} + +void Updater::ThreadProc() +{ + SetThreadName("UpdateMonitor"); + struct pollfd pfd; + pfd.fd = this->event_reader; + pfd.events = POLLIN; + + while (true) + { + int ret = poll(&pfd, 1, std::chrono::duration_cast(updateRate).count()); // 1000 ms timeout + + if (ret == -1) + { + Lv2Log::error("Updater: Poll error."); + break; + } + else if (ret == 0) + { + CheckForUpdate(true); + } + else + { + // Event occurred + uint64_t value; + ssize_t s = read(event_reader, &value, sizeof(uint64_t)); + if (s == sizeof(uint64_t)) + { + if (value == CHECK_NOW_EVENT) + { + CheckForUpdate(true); + } + else if (value == UNCACHED_CHECK_NOW_EVENT) + { + CheckForUpdate(false); + } + else + { + break; + } + } + } + } +} + +class GithubAsset +{ +public: + GithubAsset(json_variant &v); + std::string name; + std::string browser_download_url; + std::string updated_at; +}; +class GithubRelease +{ +public: + GithubRelease(json_variant &v); + + const GithubAsset *GetDownloadForCurrentArchitecture() const; + const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; + + bool draft = true; + bool prerelease = true; + std::string name; + std::string url; + std::string version; + std::string body; + std::vector assets; + std::string published_at; +}; + +GithubAsset::GithubAsset(json_variant &v) +{ + auto o = v.as_object(); + this->name = o->at("name").as_string(); + this->browser_download_url = o->at("browser_download_url").as_string(); + this->updated_at = o->at("updated_at").as_string(); +} +GithubRelease::GithubRelease(json_variant &v) +{ + auto o = v.as_object(); + this->name = o->at("name").as_string(); + this->draft = o->at("draft").as_bool(); + this->prerelease = o->at("prerelease").as_bool(); + this->body = o->at("body").as_string(); + + auto assets = o->at("assets").as_array(); + for (size_t i = 0; i < assets->size(); ++i) + { + auto &el = assets->at(i); + this->assets.push_back(GithubAsset(el)); + } + this->published_at = o->at("published_at").as_string(); +} + +static std::vector split(const std::string &s, char delimiter) +{ + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) + { + tokens.push_back(token); + } + return tokens; +} +static std::string justTheVersion(const std::string &assetName) +{ + // eg. pipedal_1.2.41_arm64.deb + auto t = split(assetName, '_'); + if (t.size() != 3) + { + throw std::runtime_error("Unable to parse version."); + } + return t[1]; +} + +int compareVersions(const std::string &l, const std::string &r) +{ + std::stringstream sl(l); + std::stringstream sr(r); + + int majorL = -1, majorR = 1, minorL = -1, + minorR = -1, buildL = -1, buildR = -1; + sl >> majorL; + sr >> majorR; + if (majorL != majorR) + { + return (majorL < majorR) ? -1 : 1; + } + char discard; + sl >> discard >> minorL; + sr >> discard >> minorR; + if (minorL != minorR) + { + return minorL < minorR ? -1 : 1; + } + sl >> discard >> buildL; + sr >> discard >> buildR; + + if (buildL != buildR) + { + return buildL < buildR ? -1 : 1; + } + return 0; +} +static std::string normalizeReleaseName(const std::string &releaseName) +{ + // e.g. "PiPedal 1.2.34 Release" -> "PiPedal v1.2.34-Release" + if (releaseName.empty()) + return ""; + + std::string result = releaseName; + + auto nPos = result.find(' '); + if (nPos == std::string::npos) + { + return result; + } + ++nPos; + if (nPos >= result.length()) + { + return result; + } + char c = releaseName[nPos]; + if (c >= '0' && c <= '9') + { + result.insert(result.begin() + nPos, 'v'); + } + nPos = result.find(' ', nPos); + if (nPos != std::string::npos) + { + result.at(nPos) = '-'; + } + return result; +} + +static bool IsCacheValid(const UpdateStatus &updateStatus) +{ + if (!updateStatus.IsValid() || !updateStatus.IsOnline()) + { + return false; + } + auto now = std::chrono::system_clock::now(); + auto validStart = updateStatus.LastUpdateTime(); + auto validEnd = validStart + CACHE_DURATION; + return now >= validStart && now < validEnd; +} + +UpdateRelease Updater::getUpdateRelease( + const std::vector &githubReleases, + const std::string ¤tVersion, + const UpdateReleasePredicate &predicate) +{ + for (const auto &githubRelease : githubReleases) + { + auto *asset = githubRelease.GetDownloadForCurrentArchitecture(); + if (!asset) + continue; + auto *pgpKey = githubRelease.GetGpgKeyForAsset(asset->name); + if (!pgpKey) + { + continue; + } + + if (!predicate(githubRelease)) + continue; + UpdateRelease updateRelease; + updateRelease.upgradeVersion_ = justTheVersion(asset->name); + updateRelease.updateAvailable_ = compareVersions(currentVersion, updateRelease.upgradeVersion_) < 0; + updateRelease.upgradeVersionDisplayName_ = normalizeReleaseName(githubRelease.name); + updateRelease.assetName_ = asset->name; + updateRelease.updateUrl_ = asset->browser_download_url; + updateRelease.gpgSignatureUrl_ = pgpKey->browser_download_url; + return updateRelease; + } + return UpdateRelease(); +} + +static void CheckUpdateHttpResponse(std::string errorCode) +{ + if (errorCode.starts_with("%")) + { + errorCode = errorCode.substr(1); + } + int code = -999; + { + std::istringstream ss{errorCode}; + ss >> code; + } + if (code == -999) + { + throw std::runtime_error(SS("Invalid curl response: " << errorCode)); + } + if (code == 200) + { + return; + } + if (code == 0) + { + throw std::runtime_error("PiPedal server can't reach the internet."); + } + + { + std::string message = SS("HTTP error " << code << ""); + throw std::runtime_error(message); + } +} + +void Updater::CheckForUpdate(bool useCache) +{ + UpdateStatus updateResult; + + { + std::lock_guard lock{mutex}; + if (useCache && IsCacheValid(cachedUpdateStatus)) + { + this->currentResult = cachedUpdateStatus; + this->currentResult.UpdatePolicy(this->updatePolicy); + if (listener) + { + listener(this->currentResult); + } + return; + } + updateResult = this->currentResult; + } + + const std::string responseOption = "-w \"%{response_code}\""; +#ifdef WIN32 + responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. +#endif + + std::string args = SS("-s -L " << responseOption << " " << GITHUB_RELEASES_URL); + + updateResult.errorMessage_ = ""; + + try + { + auto result = sysExecForOutput("curl", args); + if (result.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error("Server has no internet access."); + } + else + { + if (result.output.length() == 0) + { + throw std::runtime_error("Server has no internet access."); + } + std::stringstream ss(result.output); + json_reader reader(ss); + json_variant vResult(reader); + + if (vResult.is_object()) + { + // an HTML error. + updateResult.isOnline_ = false; + auto o = vResult.as_object(); + std::string message = o->at("message").as_string(); + auto status_code = o->at("status_code").as_int64(); + throw std::runtime_error(SS("Service error. ()" << status_code << ": " << message << ")")); + } + else + { + json_variant::array_ptr vArray = vResult.as_array(); + + std::vector releases; + for (size_t i = 0; i < vArray->size(); ++i) + { + auto &el = vArray->at(0); + GithubRelease release{el}; + if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr) + { + releases.push_back(std::move(release)); + } + } + std::sort( + releases.begin(), + releases.end(), + [](const GithubRelease &left, const GithubRelease &right) + { + return left.published_at > right.published_at; // latest date first. + }); + updateResult.releaseOnlyRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return !githubRelease.prerelease && + githubRelease.name.find("Release") != std::string::npos; + }); + + updateResult.releaseOrBetaRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return !githubRelease.prerelease && + (githubRelease.name.find("Release") != std::string::npos || + githubRelease.name.find("Beta") != std::string::npos); + }); + updateResult.devRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return true; + }); +#ifdef TEST_UPDATE + updateResult.releaseOrBetaRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.41-Beta"; + updateResult.devRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.39-Experimental"; + updateResult.devRelease_.upgradeVersion_ = "1.2.39"; + updateResult.devRelease_.updateAvailable_ = false; +#endif + updateResult.isValid_ = true; + updateResult.isOnline_ = true; + } + } + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Failed to fetch update info. " << e.what())); + updateResult.errorMessage_ = e.what(); + updateResult.isValid_ = false; + updateResult.isOnline_ = false; + } + { + std::lock_guard lock{mutex}; + updateResult.UpdatePolicy(this->updatePolicy); + this->currentResult = updateResult; + SetCachedUpdateStatus(this->currentResult); + if (listener) + { + listener(this->currentResult); + } + } +} +bool UpdateRelease::operator==(const UpdateRelease &other) const +{ + return (updateAvailable_ == other.updateAvailable_) && + (upgradeVersion_ == other.upgradeVersion_) && + (upgradeVersionDisplayName_ == other.upgradeVersionDisplayName_) && + (assetName_ == other.assetName_) && + (updateUrl_ == other.updateUrl_); +} + +bool UpdateStatus::operator==(const UpdateStatus &other) const +{ + return (lastUpdateTime_ == other.lastUpdateTime_) && + (isValid_ == other.isValid_) && + (errorMessage_ == other.errorMessage_) && + (isOnline_ == other.isOnline_) && + (currentVersion_ == other.currentVersion_) && + (currentVersionDisplayName_ == other.currentVersionDisplayName_) && + (updatePolicy_ == other.updatePolicy_) && + (releaseOnlyRelease_ == other.releaseOnlyRelease_) && + (releaseOrBetaRelease_ == other.releaseOrBetaRelease_) && + (devRelease_ == other.devRelease_); +} + +UpdatePolicyT Updater::GetUpdatePolicy() +{ + std::lock_guard lock{mutex}; + return updatePolicy; +} +void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy) +{ + std::lock_guard lock{mutex}; + if (updatePolicy == this->updatePolicy) + return; + this->updatePolicy = updatePolicy; + this->currentResult.UpdatePolicy(updatePolicy); + SetCachedUpdateStatus(this->currentResult); + if (listener) + { + listener(currentResult); + } +} +void Updater::ForceUpdateCheck() +{ + uint64_t value = UNCACHED_CHECK_NOW_EVENT; + write(this->event_writer, &value, sizeof(uint64_t)); +} + +UpdateStatus::UpdateStatus() +{ + currentVersion_ = PROJECT_VER; + currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION; + +#ifdef TEST_UPDATE + // uncomment this line to test upgrading. + currentVersion_ = "1.2.39"; + currentVersionDisplayName_ = "PiPedal 1.2.39-Debug"; +#endif +} + +void UpdateStatus::ResetCurrentVersion() +{ + currentVersion_ = PROJECT_VER; + currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION; + +#ifdef TEST_UPDATE + // uncomment this line to test upgrading. + currentVersion_ = "1.2.39"; + currentVersionDisplayName_ = "PiPedal 1.2.39-Debug"; +#endif +} + +std::chrono::system_clock::time_point UpdateStatus::LastUpdateTime() const +{ + std::chrono::system_clock::duration duration{this->lastUpdateTime_}; + std::chrono::system_clock::time_point tp{duration}; + return tp; +} + +void UpdateStatus::LastUpdateTime(const std::chrono::system_clock::time_point &timePoint) +{ + this->lastUpdateTime_ = timePoint.time_since_epoch().count(); +} + +const GithubAsset *GithubRelease::GetGpgKeyForAsset(const std::string &name) const +{ + std::string targetName = name + ".asc"; + + for (auto &asset : assets) + { + if (asset.name == targetName) + { + return &asset; + } + } + return nullptr; +} +const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const +{ + // deb package names end in {DEBIAN_ARCHITECTURE}.deb + // pipedal build gets this value from `dpkg --print-architecture` +#ifndef DEBIAN_ARCHITECTURE // deb package names end in {DEBIAN_ARCHITECTURE}.deb +#error DEBIAN_ARCHITECTURE not defined +#endif + std::string downloadEnding = SS("_" << (DEBIAN_ARCHITECTURE) << ".deb"); + + for (auto &asset : assets) + { + if (asset.name.ends_with(downloadEnding)) + { + return &asset; + } + } + return nullptr; +} + +UpdateRelease::UpdateRelease() +{ +} + +std::string Updater::GetSignatureUrl(const std::string &url) +{ + + // partialy whitelisting, partly avoiding having to parse a URL. + if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOnlyRelease_.GpgSignatureUrl(); + } + if (this->currentResult.releaseOrBetaRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOrBetaRelease_.GpgSignatureUrl(); + } + if (this->currentResult.devRelease_.UpdateUrl() == url) + { + return this->currentResult.devRelease_.GpgSignatureUrl(); + } + throw std::runtime_error("Permission denied. No signature URL."); +} + +std::string Updater::GetUpdateFilename(const std::string &url) +{ + + // partialy whitelisting, partly avoiding having to parse a URL. + if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOnlyRelease_.AssetName(); + } + if (this->currentResult.releaseOrBetaRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOrBetaRelease_.AssetName(); + } + if (this->currentResult.devRelease_.UpdateUrl() == url) + { + return this->currentResult.devRelease_.AssetName(); + } + throw std::runtime_error("Permission denied. Invalid url."); +} +static std::string unCRLF(const std::string &text) +{ + std::ostringstream ss; + for (char c : text) + { + if (c == '\r') + continue; + if (c == '\n') + { + ss << '/'; + } + else + { + ss << c; + } + } + return ss.str(); +} + +static void removeOldSiblings(int numberToKeep, const std::filesystem::path &fileToKeep) +{ + + auto extension = fileToKeep.extension(); + auto directory = fileToKeep.parent_path(); + if (directory.empty()) + return; // superstition. + struct RemoveEntry + { + fs::path path; + fs::file_time_type time; + }; + std::vector entries; + for (const auto &dirEntry : fs::directory_iterator(directory)) + { + if (dirEntry.is_regular_file()) + { + auto thisPath = dirEntry.path(); + if (thisPath != fileToKeep) + { + if (thisPath.extension() == extension) + { + entries.push_back( + RemoveEntry{ + .path = thisPath, + .time = dirEntry.last_write_time()}); + } + } + } + } + std::sort( + entries.begin(), entries.end(), + [](const RemoveEntry &left, const RemoveEntry &right) + { + return left.time > right.time; // by time descending + }); + for (size_t i = numberToKeep; i < entries.size(); ++i) + { + fs::remove(entries[i].path); + } +} + +static std::string getFingerprint(const std::string &gpgText) +{ + std::string keyPosition = "using RSA key "; + size_t nPos = gpgText.find(keyPosition); + if (nPos == std::string::npos) + { + return ""; + } + nPos += keyPosition.length(); + while (nPos < gpgText.length() && gpgText[nPos] == ' ') + { + ++nPos; + } + auto start = nPos; + + while (true) + { + char c = gpgText[nPos]; + if (nPos >= gpgText.length() || c == ' ' || c == '\r' || c == '\n' || c == '\t') + { + break; + } + ++nPos; + } + return gpgText.substr(start, nPos - start); +} +static bool IsSignatureGood(const std::string&gpgText) +{ + std::string originPosition = "gpg: Good signature from \""; + + size_t nPos = gpgText.find(originPosition); + return nPos != std::string::npos; +} +static std::string getAddress(const std::string &gpgText) +{ + std::string originPosition = "gpg: Good signature from \""; + + size_t nPos = gpgText.find(originPosition); + if (nPos == std::string::npos) + { + return ""; + } + nPos += originPosition.length(); + auto start = nPos; + while (true) + { + if (nPos >= gpgText.length() || gpgText[nPos] == '\"' || gpgText[nPos] == '\r' || gpgText[nPos] == '\n') + { + break; + } + ++nPos; + } + return gpgText.substr(start, nPos - start); +} +void Updater::ValidateSignature(const std::filesystem::path &file, const std::filesystem::path &signatureFile) +{ + // sudo gpg --home /var/pipedal/config/gpg --verify pipedal_1.2.41_arm64.deb.asc pipedal_1.2.41_arm64.deb + // gpg: assuming signed data in 'pipedal_1.2.41_arm64.deb' + // gpg: Signature made Tue 27 Aug 2024 09:25:06 PM EDT + // gpg: using RSA key 381124E2BB4478D225D2313B2AEF3F7BD53EAA59 + // gpg: Good signature from "Robin Davies " [ultimate] + std::ostringstream ss; + ss << "--home " << PGP_UPDATE_KEYRING_PATH + << " --verify " + << signatureFile << " " << file; + + Lv2Log::info(SS("/usr/bin/gpg " << ss.str())); + auto gpgOutput = sysExecForOutput("/usr/bin/gpg", ss.str()); + if (gpgOutput.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error("Update file is not correctly signed."); + } + const std::string &gpgText = gpgOutput.output; + + if (!IsSignatureGood(gpgText)) + { + throw std::runtime_error("Update signature is not valid."); + } + std::string keyId = getFingerprint(gpgText); + + Lv2Log::info(unCRLF(gpgText)); // yyy delete me. + + if (keyId != UPDATE_GPG_FINGERPRINT) + { + throw std::runtime_error(SS("Update signature has the wrong fingerprint: " << keyId)); + } + std::string origin = getAddress(gpgText); + if (origin != UPDATE_GPG_ADDRESS) + { + throw std::runtime_error(SS("Update signature has an incorrect address." << origin)); + } +} + +static bool badOutput(const std::filesystem::path &filePath) +{ + if (!fs::is_regular_file(filePath)) + { + return true; + } + return fs::file_size(filePath) == 0; +} + +static void checkCurlHttpResponse(std::string errorCode) +{ + if (errorCode.starts_with("%")) + { + errorCode = errorCode.substr(1); + } + int code = -999; + { + std::istringstream ss{errorCode}; + ss >> code; + } + if (code == -999) + { + throw std::runtime_error(SS("Download failed. Invalid curl response: " << errorCode)); + } + if (code == 200) + { + return; + } + if (code == 0) + { + throw std::runtime_error("PiPedal server can't reach the internet."); + } + + { + std::string message = SS("Download failed (HTTP error " << code << ")"); + throw std::runtime_error(message); + } +} +void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) +{ + std::string filename, signatureUrl; + { + std::lock_guard lock{mutex}; + filename = GetUpdateFilename(url); + signatureUrl = GetSignatureUrl(url); + } + + // Only permit downloading of updates from the github releases for the pipedal project. + // I don't think this will get past GetUpdateFileName, but defense in depth. We will + // not download from anywhere we don't trust. + if (!WhitelistDownloadUrl(url)) + { + throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << url)); + } + if (!WhitelistDownloadUrl(signatureUrl)) + { + throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << signatureUrl)); + } + auto downloadDirectory = WORKING_DIRECTORY / "downloads"; + std::filesystem::create_directories(downloadDirectory); + + auto downloadFilePath = downloadDirectory / filename; + auto downloadSignaturePath = downloadDirectory / SS(filename << ".asc"); + + try + { + fs::remove(downloadFilePath); + fs::remove(downloadSignaturePath); + + const std::string responseOption = "-w \"%{response_code}\""; +#ifdef WIN32 + responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. +#endif + std::string args = SS("-s -L " << responseOption << " " << url << " -o " << downloadFilePath.c_str()); + Lv2Log::info(SS("/usr/bin/curl " << args)); // yyy delete me. + auto curlOutput = sysExecForOutput("/usr/bin/curl", args); + if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadFilePath)) + { + Lv2Log::error(SS("Update download failed. " << unCRLF(curlOutput.output))); + throw std::runtime_error("PiPedal server does not have access to the internet."); + } + checkCurlHttpResponse(curlOutput.output); + + args = SS("-s -L " << responseOption << " " << signatureUrl << " -o " << downloadSignaturePath.c_str()); + Lv2Log::info(SS("/usr/bin/curl " << args)); + + curlOutput = sysExecForOutput("/usr/bin/curl", args); + if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadSignaturePath)) + { + Lv2Log::error(SS("Update download failed. " << unCRLF(curlOutput.output))); + throw std::runtime_error("PiPedal server does not have access to the internet."); + } + checkCurlHttpResponse(curlOutput.output); + + try + { + removeOldSiblings(2, downloadFilePath); + removeOldSiblings(2, downloadSignaturePath); + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Can't remove download siblings" << e.what())); + // and carry on. + } + + // Can't only be performed under pipedal_d or root accouts. + // This is as far as you can go while debugging. + + // The admin process will actually check the signature again running as root; but this is our last + // chance to give a reasonable error message, so do it here as well, because any subsequent errors + // can only go to systemd logs. + ValidateSignature(downloadFilePath, downloadSignaturePath); + *file = downloadFilePath; + *signatureFile = downloadSignaturePath; + return; + } + catch (const std::exception &e) + { + std::filesystem::remove(downloadFilePath); + std::filesystem::remove(downloadSignaturePath); + throw; + } +} +/////////////////////////////////////////////////////////////////////////////////////////////// + +JSON_MAP_BEGIN(UpdateRelease) +JSON_MAP_REFERENCE(UpdateRelease, updateAvailable) +JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion) +JSON_MAP_REFERENCE(UpdateRelease, upgradeVersionDisplayName) +JSON_MAP_REFERENCE(UpdateRelease, assetName) +JSON_MAP_REFERENCE(UpdateRelease, updateUrl) +JSON_MAP_REFERENCE(UpdateRelease, gpgSignatureUrl) +JSON_MAP_END(); + +JSON_MAP_BEGIN(UpdateStatus) +JSON_MAP_REFERENCE(UpdateStatus, lastUpdateTime) +JSON_MAP_REFERENCE(UpdateStatus, isValid) +JSON_MAP_REFERENCE(UpdateStatus, errorMessage) +JSON_MAP_REFERENCE(UpdateStatus, isOnline) +JSON_MAP_REFERENCE(UpdateStatus, currentVersion) +JSON_MAP_REFERENCE(UpdateStatus, currentVersionDisplayName) +JSON_MAP_REFERENCE(UpdateStatus, updatePolicy) +JSON_MAP_REFERENCE(UpdateStatus, releaseOnlyRelease) +JSON_MAP_REFERENCE(UpdateStatus, releaseOrBetaRelease) +JSON_MAP_REFERENCE(UpdateStatus, devRelease) +JSON_MAP_END(); diff --git a/src/Updater.hpp b/src/Updater.hpp new file mode 100644 index 0000000..e0c3325 --- /dev/null +++ b/src/Updater.hpp @@ -0,0 +1,158 @@ +// Copyright (c) 2024 Robin 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 +#include +#include "json.hpp" +#include + +class GithubRelease; + +namespace pipedal +{ + + class Updater; + + enum class UpdatePolicyT + { + // ordinal values must not change, as files depend on them. + // must match declaration in Updater.tsx + + ReleaseOnly = 0, + ReleaseOrBeta = 1, + Development = 2, + Disabled = 3 + + }; + class UpdateRelease + { + private: + friend class Updater; + bool updateAvailable_ = false; + std::string upgradeVersion_; // just the version. + std::string upgradeVersionDisplayName_; // display name for the version. + std::string assetName_; // filename only + std::string updateUrl_; // url from which to download the .deb file. + std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. + public: + UpdateRelease(); + + bool UpdateAvailable() const { return updateAvailable_; } + const std::string &UpgradeVersion() const { return upgradeVersion_; } + const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } + const std::string &AssetName() const { return assetName_; } + const std::string &UpdateUrl() const { return updateUrl_; } + const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} + bool operator==(const UpdateRelease &other) const; + DECLARE_JSON_MAP(UpdateRelease); + }; + + class UpdateStatus + { + private: + friend class Updater; + std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0; + + bool isValid_ = false; + std::string errorMessage_; + bool isOnline_ = false; + std::string currentVersion_; + std::string currentVersionDisplayName_; + + int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta); + UpdateRelease releaseOnlyRelease_; + UpdateRelease releaseOrBetaRelease_; + UpdateRelease devRelease_; + + public: + UpdateStatus(); + std::chrono::system_clock::time_point LastUpdateTime() const; + void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint); + + void ResetCurrentVersion(); + bool IsValid() const { return isValid_; } + const std::string &ErrorMessage() const { return errorMessage_; } + bool IsOnline() const { return isOnline_; } + const std::string &CurrentVersion() const { return currentVersion_; } + const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; } + UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; } + void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; } + + const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; } + const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; } + const UpdateRelease &DevRelease() const { return devRelease_; } + bool operator==(const UpdateStatus &other) const; + + DECLARE_JSON_MAP(UpdateStatus); + }; + + class Updater + { + public: + Updater(); + ~Updater(); + + static void ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile); + + using UpdateListener = std::function; + + void SetUpdateListener(UpdateListener &&listener); + void CheckNow(); + void Stop(); + + UpdatePolicyT GetUpdatePolicy(); + void SetUpdatePolicy(UpdatePolicyT updatePolicy); + void ForceUpdateCheck(); + void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile); + UpdateStatus GetCurrentStatus() const { return this->currentResult; } + private: + std::string GetUpdateFilename(const std::string &url); + std::string GetSignatureUrl(const std::string &url); + + UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; + using UpdateReleasePredicate = std::function; + + UpdateRelease getUpdateRelease( + const std::vector &githubReleases, + const std::string ¤tVersion, + const UpdateReleasePredicate &predicate); + + UpdateStatus cachedUpdateStatus; + bool stopped = false; + using clock = std::chrono::steady_clock; + + int event_reader = -1; + int event_writer = -1; + void ThreadProc(); + void CheckForUpdate(bool useCache); + UpdateListener listener; + + std::unique_ptr thread; + std::mutex mutex; + + bool hasInfo = false; + UpdateStatus currentResult; + static clock::duration updateRate; + }; +} \ No newline at end of file diff --git a/src/UpdaterSecurity.hpp b/src/UpdaterSecurity.hpp new file mode 100644 index 0000000..774ee10 --- /dev/null +++ b/src/UpdaterSecurity.hpp @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2022 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 + +/* + PiPedal uses whitelisting and GPG signatures to verify that updates are valid. + You must modify these defines appropriately if you want to host a fork. + + If you don't, PiPedal will prompt to update your installs with the latest + installs published on github/rerdavies/pipedal. +*/ + +#ifndef GITHUB_PROJECT +#define GITHUB_PROJECT "rerdavies/pipedal" +#endif + + +// public key info for the private key you used to sign .deb files. + +#ifndef UPDATE_GPG_ADDRESS +#define UPDATE_GPG_ADDRESS "Robin Davies " +#endif + +#ifndef UPDATE_GPG_FINGERPRINT +#define UPDATE_GPG_FINGERPRINT "381124E2BB4478D225D2313B2AEF3F7BD53EAA59" +#endif + +// Configuration for downloading of updates. + +#define GITHUB_DOWNLOAD_PREFIX "https://github.com/" GITHUB_PROJECT "/releases/" + +#define GITHUB_RELEASES_URL "https://api.github.com/repos/" GITHUB_PROJECT "/releases" + +#define PGP_UPDATE_KEYRING_PATH "/var/pipedal/config/gpg" + +inline bool WhitelistDownloadUrl(const std::string &downloadUrl) +{ + return downloadUrl.starts_with(GITHUB_DOWNLOAD_PREFIX); +} \ No newline at end of file diff --git a/src/UpdaterTest.cpp b/src/UpdaterTest.cpp new file mode 100644 index 0000000..7262ae9 --- /dev/null +++ b/src/UpdaterTest.cpp @@ -0,0 +1,52 @@ +// Copyright (c) 2022 Robin 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 "pch.h" +#include "catch.hpp" +#include "Updater.hpp" +#include "json.hpp" + +using namespace std; + +using namespace pipedal; + +TEST_CASE( "updater test", "[updater]" ) { + int nCalls = 0; + cout << "------ upater test ----" << endl; + { Updater updater; + + + updater.SetUpdateListener( + [&nCalls](const UpdateStatus&updateStatus) mutable + { + + cout << "updateStatus:" << endl; + json_writer writer(cout,false); + writer.write(updateStatus); + cout << endl; + ++nCalls; + } + ); + } + REQUIRE(nCalls == 1); + + + + +} \ No newline at end of file diff --git a/src/jsonTest.cpp b/src/jsonTest.cpp index e71a6c6..cdf88ff 100644 --- a/src/jsonTest.cpp +++ b/src/jsonTest.cpp @@ -158,11 +158,40 @@ TEST_CASE("json smart ptrs", "[json_smart_ptrs][Build][Dev]") } } +template +U & VariantAs(json_variant& v) +{ + throw std::runtime_error("Missing specialization."); +} + + template <> + inline json_null &VariantAs(json_variant& v) { return v.as_null(); } + + template <> + inline bool &VariantAs(json_variant& v) { return v.as_bool(); } + + template <> + inline double &VariantAs(json_variant& v) { return v.as_number(); } + + template <> + inline std::string &VariantAs(json_variant& v) { return v.as_string(); } + + template <> + inline std::shared_ptr &VariantAs>(json_variant& v) { return v.as_object(); } + + template <> + inline std::shared_ptr &VariantAs>(json_variant& v) { return v.as_array(); } + + template <> + inline json_variant &VariantAs(json_variant& v) { return v; } + + + template void TestVariantRoundTrip(const T &value) { json_variant variant(value); - T out = variant.as(); + T &out = VariantAs(variant); REQUIRE(out == value); std::string output; diff --git a/src/main.cpp b/src/main.cpp index b9d8833..43a845b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -54,15 +54,6 @@ using namespace pipedal; #endif -bool HasAlsaDevice(const std::vector devices, const std::string &deviceId) -{ - for (auto &device : devices) - { - if (device.id_ == deviceId) - return true; - } - return false; -} class application_category : public boost::system::error_category { @@ -142,9 +133,9 @@ int main(int argc, char *argv[]) std::cout << "Usage: pipedald [] [options...]\n\n" << "Options:\n" << " -systemd: Log to systemd journals instead of to the console.\n" - << " -port: Port to listen on e.g. 0.0.0.0:80\n" + << " -port: Port to listen on e.g. 80, or 0.0.0.0:80\n" << "Example:\n" - << " pipedald /etc/pipedal/config /etc/pipedal/react -port 0.0.0.0:80 \n" + << " pipedald /etc/pipedal/config /etc/pipedal/react -port 80 \n" "\n" "Description:\n\n" " Configuration is read from /config.json\n" @@ -250,58 +241,7 @@ int main(int argc, char *argv[]) (unsigned long)getpid()); } - auto serverSettings = model.GetJackServerSettings(); - - { - // Wait for selected audio device to be initialized. - // It may take some time for ALSA to publish all available devices when rebooting. - - if (serverSettings.IsValid()) - { - // wait up to 15 seconds for the midi device to come online. - auto devices = model.GetAlsaDevices(); - bool found = false; - if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice())) - { - Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); - } - else - { - for (int i = 0; i < 5; ++i) - { - sleep(3); - devices = model.GetAlsaDevices(); - if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice())) - { - found = true; - break; - } - if (g_SigBreak) - exit(1); - if (!systemd) - { - break; - } - Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << " to come online...")); - } - if (found) - { - Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); - } - else - { - Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found.")); - } - } - } - else - { - Lv2Log::info("No ALSA device selected."); - } - - // pre-cache device info before we let audio services run. - model.GetAlsaDevices(); - } + model.WaitForAudioDeviceToComeOnline(); // only accept signals on the main thread. int sig; diff --git a/src/templates/pipedaladmind.service.template b/src/templates/pipedaladmind.service.template index 9f7e5e7..fc5afd4 100644 --- a/src/templates/pipedaladmind.service.template +++ b/src/templates/pipedaladmind.service.template @@ -8,6 +8,7 @@ Restart=always RestartSec=60 WorkingDirectory=/var/pipedal TimeoutStopSec=15 +KillMode=process [Install] diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template index 0b7f588..968b5b2 100644 --- a/src/templates/pipedald.service.template +++ b/src/templates/pipedald.service.template @@ -18,7 +18,6 @@ Restart=always TimeoutStartSec=60 RestartSec=5 TimeoutStopSec=15 - WorkingDirectory=/var/pipedal Environment=JACK_PROMISCUOUS_SERVER=audio Environment=JACK_NO_AUDIO_RESERVATION=1 diff --git a/src/templates/var_config.json b/src/templates/var_config.json new file mode 100644 index 0000000..8fd1ed3 --- /dev/null +++ b/src/templates/var_config.json @@ -0,0 +1,45 @@ +{ + +// This file overrides default settings, as set by the installer in /etc/pipdal/config.json +// +// Changes made in this file will be preserved across upgrades. +// +// To override default settings, uncomment the setting line you want to modify. +// +// You should reboot after modifying any of these settings. + + + +// Paths where PiPedal will look for LV2 Plugins. +// One or more directories, seperated by ':' + + // "lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2", + + +// whether to lock process pages into memory. should be true unless running on a very memory constrained system. +// Setting to false may cause unpredictable audio dropouts. + + // "mlock": true, + +// Number of threads the web server should use. + + // "threads" : 5, + + +// Whether to log individual http requests. (highly inadvisable, as requests are logged to the systemd log) + + // "logHttpRequests": false, + +// Log level for the web server +// { None=0,Error =1,Warning =2,Info = 3, Debug=4} + + // "logLevel": 3, + +// Maximum filesize to allow when uploading + + // "maxUploadSize": 536870912 // 512MiB + + +// does nothing. avoids chasing ','s. + "end" : true +} \ No newline at end of file diff --git a/test/UriTest.cpp b/test/UriTest.cpp index 5f9c3a8..a050bc3 100644 --- a/test/UriTest.cpp +++ b/test/UriTest.cpp @@ -22,7 +22,7 @@ #include #include -using namespace piddle; +using namespace pipedal; TEST_CASE( "uri test", "[uri]" ) {