Merge pull request #182 from rerdavies/dev

v1.2.44 Release
This commit is contained in:
Robin Davies
2024-08-28 21:24:46 -04:00
committed by GitHub
97 changed files with 4461 additions and 931 deletions
+4 -3
View File
@@ -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}",
+6 -7
View File
@@ -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")
+36 -5
View File
@@ -9,14 +9,17 @@
#include "WifiRegs.hpp"
#include "ChannelInfo.hpp"
#include "DBusLog.hpp"
#include <sys/stat.h>
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.");
}
+5 -2
View File
@@ -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<uint8_t> 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_; }
+30 -23
View File
@@ -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 <filesystem>
#include <fstream>
#include <stdexcept>
#include <grp.h>
@@ -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<ServiceConfiguration,decltype(ServiceConfiguration::MEMBER_NAME)>(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "")
new ConfigSerializer<ServiceConfiguration, decltype(ServiceConfiguration::MEMBER_NAME)>(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "")
static p2p::autoptr_vector<ConfigSerializerBase<ServiceConfiguration>>
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);
}
+39
View File
@@ -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 ));
}
}
@@ -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";
}
@@ -26,6 +26,7 @@
#include <string>
#include "ConfigSerializer.hpp"
#include <filesystem>
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;
};
}
+19 -11
View File
@@ -20,20 +20,28 @@
#pragma once
#include <string>
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();
}
+47 -53
View File
@@ -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<json_array> &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<json_array> &as_array() const;
std::shared_ptr<json_array> &as_array();
template <typename U>
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>;
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<json_variant>::iterator;
using const_iterator = std::vector<json_variant>::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>;
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<std::string, json_variant> >;
using values_t = std::vector<std::pair<std::string, json_variant>>;
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<json_null>() { return as_null(); }
template <>
inline bool &json_variant::as<bool>() { return as_bool(); }
template <>
inline double &json_variant::as<double>() { return as_number(); }
template <>
inline std::string &json_variant::as<std::string>() { return as_string(); }
template <>
inline std::shared_ptr<json_object> &json_variant::as<std::shared_ptr<json_object>>() { return as_object(); }
template <>
inline std::shared_ptr<json_array> &json_variant::as<std::shared_ptr<json_array>>() { return as_array(); }
template <>
inline json_variant &json_variant::as<json_variant>() { 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.");
}
+5
View File
@@ -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;
+2 -2
View File
@@ -6,13 +6,13 @@
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.41</a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.42</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
&nbsp;
#### 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.
&nbsp;
+20 -14
View File
@@ -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
}
+41
View File
@@ -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-----
+3 -3
View File
@@ -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.
+20
View File
@@ -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.
<img src="img/Updates-sshot.png" width="80%"/>
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:
+3 -3
View File
@@ -4,17 +4,17 @@
Download the most recent Debian (.deb) package for your platform:
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.41/pipedal_1.2.41_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.41</a>
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.42/pipedal_1.2.42_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.42</a>
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb">Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31</a>
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
<img src="GithubBanner.png" width="100%"/>
<a href="Installing.html"><i>v1.2.41</i></a>
<a href="Installing.html"><i>v1.2.42</i></a>
&nbsp;
@@ -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.
&nbsp;
+1 -1
View File
@@ -88,7 +88,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
+1 -1
View File
@@ -49,7 +49,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
mod:brand "TooB";
mod:label "TooB CabSim";
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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
@@ -49,7 +49,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
+1 -1
View File
@@ -65,7 +65,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
mod:brand "TooB";
mod:label "TooB Input";
+1 -1
View File
@@ -58,7 +58,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB";
+1 -1
View File
@@ -55,7 +55,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
+1 -1
View File
@@ -1 +1 @@
ToobAmp.so.1.1.36
ToobAmp.so.1.1.43
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
Digital emulation of a Boss BF-2 Flanger.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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.
+1 -1
View File
@@ -63,7 +63,7 @@ toobml:sagGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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.
@@ -58,7 +58,7 @@ toobNam:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
A port of Steven Atkinson's Neural Amp Modeler to LV2.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 36 ;
lv2:microVersion 43 ;
rdfs:comment """
TooB Tuner is a chromatic guitar tuner.
""" ;
+1
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"socket_server_port": 80,
"socket_server_port": 81,
"socket_server_address": "*",
"debug": true,
"max_upload_size": 536870912,
+4 -4
View File
@@ -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 (
<DialogEx tag="about" fullScreen open={this.props.open}
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
@@ -228,7 +228,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
{this.model.isAndroidHosted() && (
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
Copyright &#169; 2022 Robin Davies.
Copyright &#169; 2022-2024 Robin Davies.
</Typography>
)}
<Divider />
@@ -239,7 +239,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
{
this.model.serverVersion?.webAddresses.map((address) =>
(
<Typography display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
<Typography key={addressKey++} display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
{address}
</Typography>
))
+6 -2
View File
@@ -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 { }
}
launchExternalUrl(url:string): boolean
{
return false;
}
}
+49 -21
View File
@@ -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<
<ListItemIcon >
<RenameOutlineIcon color='inherit' className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Rename Bank' />
<ListItemText primary='Rename bank' />
</ListItem>
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
<ListItemIcon>
<SaveBankAsIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Save As New Bank' />
<ListItemText primary='Save as new bank' />
</ListItem>
<ListItem button key='EditBanks' onClick={() => { this.handleDrawerManageBanks(); }}>
<ListItemIcon>
<EditBanksIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Manage Banks...' />
<ListItemText primary='Manage banks...' />
</ListItem>
</List>
<Divider />
@@ -840,7 +861,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
</div>
</main>
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
{ (this.state.aboutDialogOpen)&&
(
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
)}
<SettingsDialog
open={this.state.isSettingsDialogOpen}
onboarding={this.state.onboarding}
@@ -872,6 +896,11 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
}
/>
<UpdateDialog open={this.state.updateDialogOpen} />
{this.state.showStatusMonitor && (<JackStatusView />)}
<DialogEx
tag="Alert"
open={this.state.alertDialogOpen}
@@ -893,14 +922,13 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
</Button>
</DialogActions>
</DialogEx>
{this.state.showStatusMonitor && (<JackStatusView />)}
<div className={classes.errorContent} style={{
display: (
this.state.displayState === State.Reconnecting
|| this.state.displayState === State.ApplyingChanges
|| this.state.displayState === State.ReloadingPlugins)
wantsLoadingScreen(this.state.displayState)
? "block" : "none"
)
}}
>
<div className={classes.errorContentMask} />
+27 -24
View File
@@ -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<typeof styles> {
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 })(
>
<SelectHoverBackground selected={bankEntry.instanceId === selectedItem} showHover={true} />
<div className={classes.itemFrame}>
<div className={classes.iconFrame}>
<img src="img/ic_bank.svg" className={classes.itemIcon} alt="" />
<div className={classes.ListItemIcon}>
<BankIcon className={classes.listIcon}/>
</div>
<div className={classes.itemLabel}>
<Typography noWrap>
<Typography noWrap variant="body2" color="textPrimary">
{bankEntry.name}
</Typography>
</div>
@@ -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 (
<DialogEx tag="bank" fullScreen open={this.props.show}
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
style={{userSelect: "none"}}
>
style={{ userSelect: "none" }}
>
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
@@ -482,8 +486,8 @@ const BankDialog = withStyles(styles, { withTheme: true })(
>
<MenuItem onClick={() => { this.handleDownloadBank(); }} >
<ListItemIcon>
<DownloadIcon className="listIcon"
/>
<DownloadIcon className={classes.listIcon}
/>
</ListItemIcon>
<ListItemText>
Download bank
@@ -494,9 +498,8 @@ const BankDialog = withStyles(styles, { withTheme: true })(
<MenuItem onClick={(e) => this.handleUploadBank()}>
<ListItemIcon>
<UploadIcon className="listIcon"
/>
<ListItemIcon >
<UploadIcon className={classes.listIcon} />
</ListItemIcon>
<ListItemText>
Upload bank
@@ -541,15 +544,15 @@ const BankDialog = withStyles(styles, { withTheme: true })(
/>
<DialogEx tag="deletePrompt" open={this.state.showDeletePrompt} onClose={() => this.handleDeletePromptClose()}
style={{userSelect: "none"}}>
style={{ userSelect: "none" }}>
<DialogContent>
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
</DialogContent>
<DialogActions>
<Button variant="dialogSecondary" onClick={()=> this.handleDeletePromptClose()} color="primary">
<Button variant="dialogSecondary" onClick={() => this.handleDeletePromptClose()} color="primary">
Cancel
</Button>
<Button variant="dialogPrimary" onClick={()=> this.handleDeletePromptOk()} color="secondary" >
<Button variant="dialogPrimary" onClick={() => this.handleDeletePromptOk()} color="secondary" >
DELETE
</Button>
+1 -1
View File
@@ -349,7 +349,7 @@ export default withStyles(styles, { withTheme: true })(
}
).catch(
(e: any) => {
this.model.showAlert(e + "");
this.model.showAlert(e.toString());
}
);
}
+1 -1
View File
@@ -138,7 +138,7 @@ export default class JackHostStatus {
<Typography variant="caption" color="inherit">{label}</Typography>
<span style={{ color: RED_COLOR }}>
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography>
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography>
</span>
{
status.temperaturemC > -100000 &&
+325 -131
View File
@@ -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<State> = new ObservableProperty<State>(State.Loading);
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
promptForUpdate: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
updateStatus: ObservableProperty<UpdateStatus> = new ObservableProperty<UpdateStatus>(new UpdateStatus());
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
@@ -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<number>("hello")
.then(clientId => {
this.clientId = clientId;
return this.getUpdateStatus(); // detects whether server has been upgraded.
})
.then((updateStatus) => {
return this.getWebSocket().request<any>("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<PresetIndex>("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<number> {
// default behaviour is to save after the currently selected preset.
if (saveAfterInstanceId === -1) {
@@ -1636,21 +1742,97 @@ export class PiPedalModel //implements PiPedalModel
});
}
getUpdateStatus(): Promise<UpdateStatus> {
return new Promise<UpdateStatus>(
(accept, reject) => {
nullCast(this.webSocket)
.request<UpdateStatus>('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<void> {
return new Promise<void>(
(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<void>('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<string[]> {
return nullCast(this.webSocket)
.request<string[]>('requestFileList', piPedalFileProperty);
}
requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
return nullCast(this.webSocket)
.request<FileEntry[]>('requestFileList2',
{relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty}
{ relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty }
);
}
deleteUserFile(fileName: string) : Promise<boolean>
{
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName);
deleteUserFile(fileName: string): Promise<boolean> {
return nullCast(this.webSocket).request<boolean>('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<boolean> {
let result = new Promise<boolean>((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<string> {
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
let result = new Promise<string>((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<void>((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<string>
{
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
return new Promise<string>((resolve, reject) => {
let ws = this.webSocket;
@@ -2295,8 +2481,8 @@ export class PiPedalModel //implements PiPedalModel
});
});
}
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise<FilePropertyDirectoryTree> {
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise<FilePropertyDirectoryTree> {
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
let ws = this.webSocket;
if (!ws) {
@@ -2306,15 +2492,14 @@ export class PiPedalModel //implements PiPedalModel
ws.request<FilePropertyDirectoryTree>(
"getFilePropertyDirectoryTree",
uiFileProperty
).then((result)=> {
).then((result) => {
resolve(new FilePropertyDirectoryTree().deserialize(result));
}).catch((e) => {
reject(e);
});
});
}
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
{
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
return new Promise<string>((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<number>
{
newPresetItem(createAfter: number): Promise<number> {
return nullCast(this.webSocket).request<number>("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;
+6 -2
View File
@@ -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)
{
+64 -1
View File
@@ -257,6 +257,9 @@ const PluginControl =
touchDown: boolean = false;
touchIdentifier: number = -1;
private lastclickTimeMs: number = 0;
private isTap: boolean = false;
onTouchStart(e: TouchEvent<SVGSVGElement>) {
}
onTouchMove(e: TouchEvent<SVGSVGElement>) {
@@ -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<SVGSVGElement>) {
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<SVGSVGElement>) {
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<SVGSVGElement>): 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;
}
}
}
+20 -2
View File
@@ -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 })(
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} >
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>System MIDI Bindings</Typography>
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>System MIDI bindings</Typography>
</div>
</ButtonBase>
@@ -748,7 +754,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
Color Theme</Typography>
Color theme</Typography>
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>
{ this.model.getTheme() === ColorTheme.Dark ? "Dark" :
(this.model.getTheme() === ColorTheme.Light ? "Light": "System")}
@@ -786,6 +792,18 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
</div>
</ButtonBase>
<ButtonBase
className={classes.setting}
onClick={() => { this.handleCheckForUpdates(); }} >
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
Check for updates...</Typography>
</div>
</ButtonBase>
<ButtonBase className={classes.setting} disabled={disableShutdown}
onClick={() => this.handleRestart()} >
<SelectHoverBackground selected={false} showHover={true} />
+285
View File
@@ -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<UpdateDialogProps, UpdateDialogState> {
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 (
<DialogEx tag="update" open={this.props.open} onClose={() => { this.handleClose(); }}
style={{ userSelect: "none" }}
>
{
(!compact) &&
(
<DialogTitle>
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }} >
<Typography style={{ flexGrow: 1, flexShrink: 1, marginRight: 20 }} noWrap>PiPedal Updates</Typography>
<Select style={{ opacity: 0.6 }} variant="standard" value={updateStatus.updatePolicy} onChange={(ev) => { this.onPolicySelected(ev.target.value); }}>
<MenuItem value={UpdatePolicyT.ReleaseOnly}>Release only</MenuItem>
<MenuItem value={UpdatePolicyT.ReleaseOrBeta}>Release or Beta</MenuItem>
<MenuItem value={UpdatePolicyT.Development}>Development</MenuItem>
<MenuItem value={UpdatePolicyT.Disable}>Disable</MenuItem>
</Select>
</div>
</DialogTitle>
)
}
{
(!compact) &&
(
<Divider />
)
}
<DialogContent>
{(upToDate) && (
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
PiPedal is up to date.
</Typography>
)
}
{(canUpgrade) && (
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
A new version of the PiPedal server is available. Would you like to update now?
</Typography>
)
}
<div style={{ display: "flex", flexFlow: "row noWrap" }}>
<div>
<Typography noWrap variant="body2" color="textSecondary" >
Current version:
</Typography>
{(showUpgradeVersion) && (
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
Update version:
</Typography>
)
}
</div>
<div style={{ flexShrink: 1, flexGrow: 1, marginLeft: 12 }}>
<Typography noWrap variant="body2" color="textSecondary" >
{updateStatus.currentVersionDisplayName}
</Typography>
{(showUpgradeVersion) && (
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
{updateRelease.upgradeVersionDisplayName}
</Typography>
)
}
</div>
</div>
{
(updateStatus.errorMessage.length !== 0) &&
(
<Typography variant="body2" style={{ marginTop: 12 }}>
{
updateStatus.errorMessage
}
</Typography>
)
}
<Button style={{ marginLeft: 16, marginTop: 12 }} onClick={() => { this.onViewReleaseNotes(); }}>View release notes</Button>
</DialogContent>
<Divider />
<DialogActions>
{
(canUpgrade) ?
(
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}>
<Button variant="dialogSecondary" onClick={() => { this.handleUpdateLater(); }}>Update later</Button>
<div style={{ flexGrow: 1, flexShrink: 1 }} />
<Button variant="dialogPrimary" onClick={() => { this.handleUpdateNow(); }}>Update now</Button>
</div>
) : (
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}
>
<div style={{ flexGrow: 1, flexShrink: 1 }} />
<Button onClick={() => { this.handleOK(); }} variant="dialogPrimary">OK</Button>
</div>
)
}
</DialogActions>
<DialogEx
tag="UpdateAlert"
open={this.state.alertDialogOpen}
onClose={() => { this.handleCloseAlert(); }}
aria-describedby="Alert Dialog"
>
<DialogContent>
<Typography variant="body2" color="secondaryText">
{
this.state.alertDialogMessage
}
</Typography>
</DialogContent>
<DialogActions>
<Button variant="dialogPrimary" onClick={() => { this.handleCloseAlert(); }} autoFocus>
OK
</Button>
</DialogActions>
</DialogEx>
</DialogEx >
);
}
}
+123
View File
@@ -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.
}
}
+69 -33
View File
@@ -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<typeof styles> {
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<SVGSVGElement>) {
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<SVGSVGElement>)
{
releaseCapture(e: PointerEvent<SVGSVGElement>) {
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<SVGSVGElement>) {
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<SVGSVGElement>): 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<SVGSVGElement>): 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 (
<DialIcon ref={this.imgRef} className={classes.dialIcon}
style={{
transform: this.getDefaultRotationTransform(),
width: this.props.size, height: this.props.size,
transform: this.getDefaultRotationTransform(),
width: this.props.size, height: this.props.size,
}}
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
+9
View File
@@ -195,6 +195,14 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
);
}
}
onDoubleTap() {
let controlInfo = this.props.controlInfo;
if (!controlInfo) return;
let instanceId = controlInfo?.instanceId;
let uiControl = controlInfo?.uiControl
this.model.setPedalboardControl(instanceId,uiControl.symbol,uiControl.default_value);
}
render() {
if (!this.props.controlInfo) {
@@ -241,6 +249,7 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
{uiControl.isDial() ? (
<ZoomedDial size={200} controlInfo={this.props.controlInfo}
onDoubleTap={()=>{this.onDoubleTap();}}
onPreviewValue={(v) => {
this.setState({ value: v });
}}
+8 -1
View File
@@ -1 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" >
<g>
<rect fill="none" height="24" width="24"/>
</g>
<g>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 358 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M18.993,8.993L18.99,5c0-1.1-0.891-2-1.99-2h-4h-3H6C4.9,3,4.01,3.9,4.01,5L4.007,8.993L4,9v10c0,1.1,0.891,2,1.99,2H6h11
h0.01c1.1,0,1.99-0.9,1.99-2V9L18.993,8.993z M17,10v9H6v-9V5h5h1h5V10z"/>
<rect x="8" y="7" width="2" height="2"/>
<rect x="12" y="7" width="3" height="2"/>
<rect x="8" y="11" width="2" height="2"/>
<rect x="12" y="11" width="3" height="2"/>
<rect x="8" y="15" width="2" height="2"/>
<rect x="12" y="15" width="3" height="2"/>
</svg>

After

Width:  |  Height:  |  Size: 939 B

Executable
+10
View File
@@ -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 ..
+19 -6
View File
@@ -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());
}
+2 -1
View File
@@ -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;
+192
View File
@@ -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 <fstream>
#include <stdexcept>
#include "ss.hpp"
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include "Lv2Log.hpp"
#include <string.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <signal.h>
#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<char *> argv;
char *p = const_cast<char *>(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<std::string> 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");
}
+28
View File
@@ -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 <filesystem>
namespace pipedal
{
void AdminInstallUpdate(const std::filesystem::path path);
}
+11 -1
View File
@@ -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 " <<path.string());
int rc = system(cmd.c_str());
}
class AdminServer
{
private:
@@ -259,7 +265,11 @@ private:
}
try
{
if (command == "UnmonitorGovernor")
if (command == "InstallUpdate")
{
std::filesystem::path path = args;
InstallUpdate(path);
} else if (command == "UnmonitorGovernor")
{
StopGovernorMonitorThread();
result = 0;
+42 -6
View File
@@ -239,10 +239,10 @@ namespace pipedal
unsigned int periods = 0;
snd_pcm_hw_params_t *captureHwParams;
snd_pcm_sw_params_t *captureSwParams;
snd_pcm_hw_params_t *playbackHwParams;
snd_pcm_sw_params_t *playbackSwParams;
snd_pcm_hw_params_t *captureHwParams = nullptr;
snd_pcm_sw_params_t *captureSwParams = nullptr;
snd_pcm_hw_params_t *playbackHwParams = nullptr;
snd_pcm_sw_params_t *playbackSwParams = nullptr;
bool capture_and_playback_not_synced = false;
@@ -1469,6 +1469,32 @@ namespace pipedal
Lv2Log::error("ALSA audio thread terminated abnormally.");
}
this->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<std::string> &inputAudioPorts,
std::vector<std::string> &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;
}
+3
View File
@@ -208,6 +208,9 @@ public:
Oscillator oscillator;
LatencyMonitor latencyMonitor;
virtual void OnAudioTerminated() {
}
virtual void OnAudioStopped() {
}
+2 -2
View File
@@ -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
+1
View File
@@ -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;
};
+167 -143
View File
@@ -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 <fstream>
#include "Lv2EventBufferWriter.hpp"
#include "InheritPriorityMutex.hpp"
#include <atomic>
#ifdef __linux__
#include <sched.h>
@@ -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> audioDriver;
std::recursive_mutex mutex;
int64_t overrunGracePeriodSamples = 0;
@@ -282,7 +298,9 @@ private:
SystemMidiBinding prevMidiBinding;
JackChannelSelection channelSelection;
bool active = false;
std::atomic<bool> active = false;
std::atomic<bool> audioStopped = false;
std::atomic<bool> isDummyAudioDriver = false;
std::shared_ptr<Lv2Pedalboard> currentPedalboard;
std::vector<std::shared_ptr<Lv2Pedalboard>> 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<MidiBinding>&bindings);
virtual void SetSystemMidiBindings(const std::vector<MidiBinding> &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<clock_duration>(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<Lv2Pedalboard> currentpedalboard;
std::shared_ptr<Lv2Pedalboard> 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<AudioDriver>(CreateDummyAudioDriver(this));
} else {
this->isDummyAudioDriver = false;
this->audioDriver = std::unique_ptr<AudioDriver>(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<int64_t> &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<MidiBinding>&bindings)
void AudioHostImpl::SetSystemMidiBindings(const std::vector<MidiBinding> &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)
+1 -1
View File
@@ -166,7 +166,7 @@ public:
class JackHostStatus {
public:
bool active_;
bool active_ = false;
std::string errorMessage_;
bool restarting_;
uint64_t underruns_;
+25 -4
View File
@@ -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)
+198 -158
View File
@@ -38,6 +38,7 @@
#include <random>
#include "AudioConfig.hpp"
#include "WifiChannelSelectors.hpp"
#include "PiPedalConfiguration.hpp"
#include <grp.h>
#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<std::string> 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<std::string> 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<std::string> 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<char *> args;
std::string pkexec = "/usr/bin/sudo"; // staged because "ISO C++ forbids converting a string constant to std::vector<char*>::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<fs::path, fs::perms> 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 [<country_code>] \tList valid p2p channels for the current/specified country."
<< "\n\n"
// << HangingIndent() << " --enable-legacy-ap\t <country_code> <ssid> <wep_password> <channel>\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<std::string>&arguments)
static int ListP2PChannels(const std::vector<std::string> &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<std::string>&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;
+564
View File
@@ -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 <bit>
#include <memory>
#include "ss.hpp"
#include "DummyAudioDriver.hpp"
#include "JackServerSettings.hpp"
#include <thread>
#include "RtInversionGuard.hpp"
#include "PiPedalException.hpp"
#include <atomic>
#include <chrono>
#include <thread>
#include "CpuUse.hpp"
#include <alsa/asoundlib.h>
#include "Lv2Log.hpp"
#include <limits>
#include "ss.hpp"
#undef ALSADRIVER_CONFIG_DBG
#ifdef ALSADRIVER_CONFIG_DBG
#include <stdio.h>
#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<float *> activeCaptureBuffers;
std::vector<float *> activePlaybackBuffers;
std::vector<float *> captureBuffers;
std::vector<float *> 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<bool> terminateAudio_ = false;
void terminateAudio(bool terminate)
{
this->terminateAudio_ = terminate;
}
bool terminateAudio()
{
return this->terminateAudio_;
}
private:
void DummyCleanup()
{
}
private:
void AllocateBuffers(std::vector<float *> &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(&param, 0, sizeof(param));
param.sched_priority = RT_THREAD_PRIORITY;
int result = sched_setscheduler(0, SCHED_RR, &param);
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<MidiState *> 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<float *> &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<std::string> &inputAudioPorts,
std::vector<std::string> &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
+35
View File
@@ -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);
}
+7 -2
View File
@@ -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;
}
}
+8
View File
@@ -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();
+5
View File
@@ -27,6 +27,7 @@
#include <chrono>
#include <sys/eventfd.h>
#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();
+4
View File
@@ -345,6 +345,10 @@ public:
virtual void OnAudioStopped()
{
}
virtual void OnAudioTerminated()
{
}
virtual void OnProcess(size_t nFrames)
{
+70 -2
View File
@@ -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<uint16_t>::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()
+3 -44
View File
@@ -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<uint16_t>::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);
+178 -106
View File
@@ -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<std::string> 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<void(void)> &&listener)
{
this->restartListener = std::move(listener);
}
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus &updateStatus)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (this->currentUpdateStatus != updateStatus)
{
this->currentUpdateStatus = updateStatus;
FireUpdateStatusChanged(this->currentUpdateStatus);
}
}
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::vector<IPiPedalModelSubscriber *> 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<std::recursive_mutex> lock(mutex);
return updater.GetCurrentStatus();
}
void PiPedalModel::UpdateNow(const std::string &updateUrl)
{
std::lock_guard<std::recursive_mutex> 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<AlsaDeviceInfo> 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();
}
+14 -4
View File
@@ -30,6 +30,7 @@
#include <functional>
#include <filesystem>
#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<void(void)> restartListener;
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
@@ -164,9 +169,8 @@ namespace pipedal
void UpdateRealtimeVuSubscriptions();
void UpdateRealtimeMonitorPortSubscriptions();
void OnVuUpdate(const std::vector<VuUpdate> &updates);
void RestartAudio();
void RestartAudio(bool useDummyAudioDriver = false);
std::vector<RealtimePatchPropertyRequest *> 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<std::string, bool> GetFavorites() const;
void SetFavorites(const std::map<std::string, bool> &favorites);
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
void ForceUpdateCheck();
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
+33 -4
View File
@@ -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<MidiBinding> 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<ISocketFactory> pipedal::MakePiPedalSocketFactory(PiPedalModel &
void PiPedalSocketHandler::RequestShutdown(bool restart)
{
if (GetAdminClient().CanUseShutdownClient())
if (GetAdminClient().CanUseAdminClient())
{
GetAdminClient().RequestShutdown(restart);
}
+25 -5
View File
@@ -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()
+3 -1
View File
@@ -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();
+39
View File
@@ -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 <filesystem>
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;
}
+60
View File
@@ -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 <filesystem>
#include <fstream>
#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()
+39
View File
@@ -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);
};
}
+999
View File
@@ -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 <sys/eventfd.h>
#include <unistd.h>
#include <stdexcept>
#include <chrono>
#include <poll.h>
#include "Lv2Log.hpp"
#include "SysExec.hpp"
#include "json_variant.hpp"
#include "ss.hpp"
#include "Lv2Log.hpp"
#include <algorithm>
#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<Updater::clock::duration>(std::chrono::days(1));
static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast<std::chrono::system_clock::duration>(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<std::thread>([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<std::chrono::milliseconds>(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<GithubAsset> 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<std::string> split(const std::string &s, char delimiter)
{
std::vector<std::string> 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<GithubRelease> &githubReleases,
const std::string &currentVersion,
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<GithubRelease> 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<RemoveEntry> 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 <rerdavies@gmail.com>" [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();
+158
View File
@@ -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 <string>
#include <functional>
#include <memory>
#include <thread>
#include <mutex>
#include "json.hpp"
#include <chrono>
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(const UpdateStatus &upateResult)>;
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<bool(const GithubRelease &githubRelease)>;
UpdateRelease getUpdateRelease(
const std::vector<GithubRelease> &githubReleases,
const std::string &currentVersion,
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<std::thread> thread;
std::mutex mutex;
bool hasInfo = false;
UpdateStatus currentResult;
static clock::duration updateRate;
};
}
+61
View File
@@ -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 <rerdavies@gmail.com>"
#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);
}
+52
View File
@@ -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);
}
+30 -1
View File
@@ -158,11 +158,40 @@ TEST_CASE("json smart ptrs", "[json_smart_ptrs][Build][Dev]")
}
}
template <typename U>
U & VariantAs(json_variant& v)
{
throw std::runtime_error("Missing specialization.");
}
template <>
inline json_null &VariantAs<json_null>(json_variant& v) { return v.as_null(); }
template <>
inline bool &VariantAs<bool>(json_variant& v) { return v.as_bool(); }
template <>
inline double &VariantAs<double>(json_variant& v) { return v.as_number(); }
template <>
inline std::string &VariantAs<std::string>(json_variant& v) { return v.as_string(); }
template <>
inline std::shared_ptr<json_object> &VariantAs<std::shared_ptr<json_object>>(json_variant& v) { return v.as_object(); }
template <>
inline std::shared_ptr<json_array> &VariantAs<std::shared_ptr<json_array>>(json_variant& v) { return v.as_array(); }
template <>
inline json_variant &VariantAs<json_variant>(json_variant& v) { return v; }
template <typename T>
void TestVariantRoundTrip(const T &value)
{
json_variant variant(value);
T out = variant.as<T>();
T &out = VariantAs<T>(variant);
REQUIRE(out == value);
std::string output;
+3 -63
View File
@@ -54,15 +54,6 @@ using namespace pipedal;
#endif
bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> 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 <doc_root> [<web_root>] [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 <doc_root>/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;
@@ -8,6 +8,7 @@ Restart=always
RestartSec=60
WorkingDirectory=/var/pipedal
TimeoutStopSec=15
KillMode=process
[Install]
-1
View File
@@ -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
+45
View File
@@ -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
}
+1 -1
View File
@@ -22,7 +22,7 @@
#include <Uri.hpp>
#include <string>
using namespace piddle;
using namespace pipedal;
TEST_CASE( "uri test", "[uri]" ) {