PiPedal Updates
-
@@ -181,12 +182,11 @@ export default class UpdateDialog extends React.Component
- {
- (upToDate) && (
-
- PiPedal is up to date.
-
- )
+ {(upToDate) && (
+
+ PiPedal is up to date.
+
+ )
}
{(canUpgrade) && (
@@ -202,7 +202,7 @@ export default class UpdateDialog extends React.Component
Current version:
- {(canUpgrade) && (
+ {(!upToDate) && (
Update version:
@@ -213,7 +213,7 @@ export default class UpdateDialog extends React.Component
{updateStatus.currentVersionDisplayName}
- {(canUpgrade) && (
+ {(!upToDate) && (
{updateRelease.upgradeVersionDisplayName}
@@ -249,10 +249,10 @@ export default class UpdateDialog extends React.Component{ this.model.showUpdateDialog(false); }}
+
>
-
+
)
}
diff --git a/react/src/Updater.tsx b/react/src/Updater.tsx
index ad17194..7f7563a 100644
--- a/react/src/Updater.tsx
+++ b/react/src/Updater.tsx
@@ -22,6 +22,7 @@ export enum UpdatePolicyT {
ReleaseOnly = 0,
ReleaseOrBeta = 1,
Development = 2,
+ Disable = 3,
};
export function intToUpdatePolicyT(intValue: number): UpdatePolicyT {
@@ -90,6 +91,7 @@ export class UpdateStatus {
}
getActiveRelease(): UpdateRelease {
switch (this.updatePolicy) {
+ case UpdatePolicyT.Disable: // show available updates in the settings dialog.
case UpdatePolicyT.ReleaseOnly:
return this.releaseOnlyRelease;
case UpdatePolicyT.ReleaseOrBeta:
diff --git a/src/AdminClient.cpp b/src/AdminClient.cpp
index 4f77e59..7db67ca 100644
--- a/src/AdminClient.cpp
+++ b/src/AdminClient.cpp
@@ -41,7 +41,7 @@ AdminClient::~AdminClient()
{
}
-bool AdminClient::CanUseShutdownClient()
+bool AdminClient::CanUseAdminClient()
{
return true;
}
@@ -104,7 +104,7 @@ bool AdminClient::SetJackServerConfiguration(const JackServerSettings &jackServe
void AdminClient::SetWifiConfig(const WifiConfigSettings &settings)
{
- if (!CanUseShutdownClient())
+ if (!CanUseAdminClient())
{
throw PiPedalException("Can't perform this operation when debugging.");
}
@@ -121,7 +121,7 @@ void AdminClient::SetWifiConfig(const WifiConfigSettings &settings)
}
void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
{
- if (!CanUseShutdownClient())
+ if (!CanUseAdminClient())
{
throw PiPedalException("Can't perform this operation when debugging.");
}
@@ -139,7 +139,7 @@ void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
void AdminClient::SetGovernorSettings(const std::string &settings)
{
- if (!CanUseShutdownClient())
+ if (!CanUseAdminClient())
{
throw PiPedalException("Can't use AdminClient when running interactively.");
}
@@ -157,7 +157,7 @@ void AdminClient::SetGovernorSettings(const std::string &settings)
void AdminClient::MonitorGovernor(const std::string &governor)
{
- if (!CanUseShutdownClient())
+ if (!CanUseAdminClient())
{
return;
}
@@ -174,7 +174,7 @@ void AdminClient::MonitorGovernor(const std::string &governor)
}
void AdminClient::UnmonitorGovernor()
{
- if (!CanUseShutdownClient())
+ if (!CanUseAdminClient())
{
return;
}
@@ -183,3 +183,16 @@ void AdminClient::UnmonitorGovernor()
cmd << '\n';
bool ignored = WriteMessage(cmd.str().c_str());
}
+
+
+void AdminClient::InstallUpdate(const std::string&filename)
+{
+ if (!CanUseAdminClient())
+ {
+ // generally can't do amin operations when running under a debugger.
+ throw std::runtime_error("No admin client.");
+ }
+ std::stringstream cmd;
+ cmd << "InstallUpdate " << filename << '\n';
+ bool ignroed = WriteMessage(cmd.str().c_str());
+}
diff --git a/src/AdminClient.hpp b/src/AdminClient.hpp
index 0b59193..5516c90 100644
--- a/src/AdminClient.hpp
+++ b/src/AdminClient.hpp
@@ -34,7 +34,7 @@ class AdminClient {
public:
AdminClient();
~AdminClient();
- bool CanUseShutdownClient();
+ bool CanUseAdminClient();
bool RequestShutdown(bool restart);
bool SetJackServerConfiguration(const JackServerSettings & jackServerSettings);
void SetWifiConfig(const WifiConfigSettings & settings);
@@ -42,6 +42,7 @@ public:
void SetGovernorSettings(const std::string & governor);
void MonitorGovernor(const std::string &governor);
void UnmonitorGovernor();
+ void InstallUpdate(const std::string&filename);
private:
std::mutex mutex;
UnixSocket socket;
diff --git a/src/AdminInstallUpdate.cpp b/src/AdminInstallUpdate.cpp
new file mode 100644
index 0000000..0dae2cf
--- /dev/null
+++ b/src/AdminInstallUpdate.cpp
@@ -0,0 +1,157 @@
+// Copyright (c) 2024Robin Davies
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#include "AdminInstallUpdate.hpp"
+#include
+#include
+#include "ss.hpp"
+#include
+#include
+#include
+#include "Lv2Log.hpp"
+#include
+#include
+#include
+#include
+
+using namespace pipedal;
+namespace fs = std::filesystem;
+
+static fs::path ROOT_INSTALL_DIRECTORY = "/var/pipedal/updates";
+static fs::path INSTALLER_LOG_FILE_PATH = ROOT_INSTALL_DIRECTORY / "install.log";
+
+static fs::path UPDATE_DIRECTORY = ROOT_INSTALL_DIRECTORY / "downloads";
+
+static void removeSignalHandler(int signal)
+{
+ struct sigaction sa;
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0;
+ sigaction(signal, &sa, NULL);
+}
+
+static int exec(const std::string &command)
+{
+ std::string buffer = command;
+
+
+ std::vector argv;
+ char *p = const_cast(buffer.c_str());
+ while (*p)
+ {
+ char *start = p;
+ while (*p != ' ' && *p != '\0')
+ {
+ ++p;
+ }
+ argv.push_back(start);
+ while (*p == ' ')
+ {
+ *p++ = '\0';
+ }
+ }
+ argv.push_back(nullptr);
+
+ pid_t pid;
+
+ if ((pid = fork()) == 0)
+ {
+ execv(argv[0], (char *const *)argv.data());
+ write(1,"!\n",2);
+ exit(EXIT_FAILURE);
+ }
+ else
+ {
+
+ if (pid == -1)
+ {
+ write(1,"*",1);
+ perror("execv");
+ return EXIT_FAILURE;
+ }
+ int returnValue;
+ waitpid(pid, &returnValue, 0);
+
+
+ int exitStatus = WEXITSTATUS(returnValue);
+ return exitStatus;
+ }
+}
+
+void updateLog(const std::string &message)
+{
+ write(1,message.c_str(),message.length());
+ write(1,"\n",1);
+}
+void pipedal::AdminInstallUpdate(const std::filesystem::path path)
+{
+
+ try
+ {
+ 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);
+
+ updateLog("**Stopping pipedald");
+
+ exec("/usr/bin/systemctl stop pipedald");
+
+ updateLog("** Installing update");
+ int retcode = exec(cmd);
+
+
+ // in case we didn't actually update for some reason.
+ updateLog("** Starting pipedald");
+ exec("/usr/bin/systemctl start pipedald");
+ }
+ catch (const std::exception &e)
+ {
+ updateLog(e.what());
+ }
+}
\ No newline at end of file
diff --git a/src/AdminInstallUpdate.hpp b/src/AdminInstallUpdate.hpp
new file mode 100644
index 0000000..b2a4699
--- /dev/null
+++ b/src/AdminInstallUpdate.hpp
@@ -0,0 +1,28 @@
+// Copyright (c) 2024Robin Davies
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#pragma once
+
+#include
+
+namespace pipedal
+{
+ void AdminInstallUpdate(const std::filesystem::path path);
+
+}
\ No newline at end of file
diff --git a/src/AdminMain.cpp b/src/AdminMain.cpp
index 65b17ce..04d0ca4 100644
--- a/src/AdminMain.cpp
+++ b/src/AdminMain.cpp
@@ -220,6 +220,12 @@ bool setJackConfiguration(JackServerSettings serverSettings)
return true;
}
+void InstallUpdate(const std::filesystem::path path)
+{
+ std::string cmd = SS("/usr/bin/systemd-run --unit=pidal_update /usr/sbin/pipedal_update " <join();
monitorThread = nullptr;
+ close(shutdown_eventfd);
}
}
diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp
index 6495642..b2b679e 100644
--- a/src/PiPedalModel.cpp
+++ b/src/PiPedalModel.cpp
@@ -2212,8 +2212,10 @@ UpdateStatus PiPedalModel::GetUpdateStatus() {
void PiPedalModel::UpdateNow(const std::string&updateUrl)
{
- sleep(5);
- throw std::runtime_error("Not implemented yet.");
+ std::lock_guard lock(mutex);
+ auto fileName = updater.DownloadUpdate(updateUrl);
+
+ adminClient.InstallUpdate(fileName);
}
void PiPedalModel::ForceUpdateCheck() {
diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp
index f165896..bfdd22a 100644
--- a/src/PiPedalSocket.cpp
+++ b/src/PiPedalSocket.cpp
@@ -1067,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.");
}
@@ -1088,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.");
}
@@ -1978,7 +1978,7 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel &
void PiPedalSocketHandler::RequestShutdown(bool restart)
{
- if (GetAdminClient().CanUseShutdownClient())
+ if (GetAdminClient().CanUseAdminClient())
{
GetAdminClient().RequestShutdown(restart);
}
diff --git a/src/UpdateMain.cpp b/src/UpdateMain.cpp
new file mode 100644
index 0000000..971e886
--- /dev/null
+++ b/src/UpdateMain.cpp
@@ -0,0 +1,38 @@
+// Copyright (c) 2024Robin Davies
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#include "AdminInstallUpdate.hpp"
+#include "Lv2Log.hpp"
+#include "Lv2SystemdLogger.hpp"
+#include
+
+using namespace pipedal;
+
+int main(int argc, char**argv)
+{
+ Lv2Log::set_logger(MakeLv2SystemdLogger());
+
+ if (argc != 2)
+ {
+ Lv2Log::error("Invalid arguments.");
+ return EXIT_FAILURE;
+ }
+ std::filesystem::path path = argv[1];
+ AdminInstallUpdate(path);
+}
\ No newline at end of file
diff --git a/src/Updater.cpp b/src/Updater.cpp
index b0932f7..ad2d310 100644
--- a/src/Updater.cpp
+++ b/src/Updater.cpp
@@ -67,6 +67,9 @@ static UpdateStatus GetCachedUpdateStatus()
json_reader reader(f);
UpdateStatus status;
reader.read(&status);
+
+ // cached curruent version might come from a different version.
+ status.ResetCurrentVersion();
return status;
}
}
@@ -552,6 +555,19 @@ UpdateStatus::UpdateStatus()
#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_};
@@ -583,10 +599,126 @@ const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const
return nullptr;
}
+
UpdateRelease::UpdateRelease()
{
}
+std::string Updater::GetUpdateFilename(const std::string &url)
+{
+ std::lock_guard lock(mutex);
+
+ // 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)
+{
+ namespace fs = std::filesystem;
+
+ auto directory = fileToKeep.parent_path();
+ if (directory.empty()) return; // superstition.
+ struct RemoveEntry {
+ fs::path path;
+ fs::file_time_type time;
+ };
+ std::vector entries;
+ for (const auto&dirEntry : fs::directory_iterator(directory))
+ {
+ if (dirEntry.is_regular_file())
+ {
+ if (dirEntry.path() != fileToKeep)
+ {
+ dirEntry.last_write_time();
+ entries.push_back(RemoveEntry { .path = dirEntry.path(), .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);
+ }
+}
+std::filesystem::path Updater::DownloadUpdate(const std::string &url)
+{
+ namespace fs = std::filesystem;
+ std::string filename = GetUpdateFilename(url);
+ if (filename.empty())
+ {
+ throw std::runtime_error("Permission denied. Invalid url.");
+ }
+ auto downloadDirectory = WORKING_DIRECTORY / "downloads";
+ std::filesystem::create_directories(downloadDirectory);
+
+ auto downloadPath = downloadDirectory / filename;
+
+ try {
+ fs::remove(downloadPath);
+ std::string args = SS("-s -L " << url << " -o " << downloadPath << " 2>&1");
+ auto curlOutput = sysExecForOutput("curl", args);
+ if (curlOutput.exitCode != EXIT_SUCCESS)
+ {
+ Lv2Log::error(SS("Update download failed." << unCRLF(curlOutput.output)));
+ throw std::runtime_error("PiPedal server does not have access to the internet.");
+ }
+ if (!fs::exists(downloadPath) || fs::file_size(downloadPath) == 0)
+ {
+ throw std::runtime_error("Download failed.");
+ }
+ try {
+ removeOldSiblings(2, downloadPath);
+ } catch (const std::exception&e)
+ {
+ Lv2Log::error(SS("Can't remove download siblings" << e.what()));
+ // and carry on.
+ }
+ return downloadPath;
+ } catch (const std::exception &e)
+ {
+ std::filesystem::remove(downloadPath);
+ throw;
+ }
+
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
JSON_MAP_BEGIN(UpdateRelease)
JSON_MAP_REFERENCE(UpdateRelease, updateAvailable)
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion)
diff --git a/src/Updater.hpp b/src/Updater.hpp
index aad6c13..3672964 100644
--- a/src/Updater.hpp
+++ b/src/Updater.hpp
@@ -86,6 +86,7 @@ namespace pipedal
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_; }
@@ -116,7 +117,10 @@ namespace pipedal
UpdatePolicyT GetUpdatePolicy();
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
void ForceUpdateCheck();
+ std::filesystem::path DownloadUpdate(const std::string &url);
private:
+ std::string GetUpdateFilename(const std::string &url);
+
UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta;
using UpdateReleasePredicate = std::function;
diff --git a/src/templates/pipedaladmind.service.template b/src/templates/pipedaladmind.service.template
index 9f7e5e7..fc5afd4 100644
--- a/src/templates/pipedaladmind.service.template
+++ b/src/templates/pipedaladmind.service.template
@@ -8,6 +8,7 @@ Restart=always
RestartSec=60
WorkingDirectory=/var/pipedal
TimeoutStopSec=15
+KillMode=process
[Install]
diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template
index 0b7f588..968b5b2 100644
--- a/src/templates/pipedald.service.template
+++ b/src/templates/pipedald.service.template
@@ -18,7 +18,6 @@ Restart=always
TimeoutStartSec=60
RestartSec=5
TimeoutStopSec=15
-
WorkingDirectory=/var/pipedal
Environment=JACK_PROMISCUOUS_SERVER=audio
Environment=JACK_NO_AUDIO_RESERVATION=1