diff --git a/CMakeLists.txt b/CMakeLists.txt index e474c95..7323415 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(pipedal DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "PiPedal v1.2.41-Release") +set (DISPLAY_VERSION "PiPedal v1.2.41-Experimental") set (PACKAGE_ARCHITECTURE "arm64") set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 51d7072..f24f05d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,5 @@ # Release Notes -## PiPedal 1.2.41 Release +## PiPedal 1.2.41` Release This version includes the following new features: diff --git a/react/public/var/config.json b/react/public/var/config.json index f808ca9..04d3a08 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 8080, + "socket_server_port": 80, "socket_server_address": "*", "debug": true, "max_upload_size": 536870912, diff --git a/react/src/AboutDialog.tsx b/react/src/AboutDialog.tsx index 848dfce..395e06b 100644 --- a/react/src/AboutDialog.tsx +++ b/react/src/AboutDialog.tsx @@ -156,7 +156,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( }) .catch((err) => { // ok in debug builds. File doesn't get placed until install time. - console.log("Failed to fetch open-source notices. " + err); + console.log("Failed to fetch open-source notices. " + err.toString()); }); } } diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index 989ffaf..3eb8fb7 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -358,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())); } @@ -383,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()); }); } @@ -407,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()); }); } @@ -896,12 +896,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); } } /> - { - (this.state.updateDialogOpen) - && ( - - ) - } + {this.state.showStatusMonitor && ()} diff --git a/react/src/BankDialog.tsx b/react/src/BankDialog.tsx index eb61160..edd387e 100644 --- a/react/src/BankDialog.tsx +++ b/react/src/BankDialog.tsx @@ -182,7 +182,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 +266,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( }); }) .catch((error) => { - this.model.showAlert(error); + this.model.showAlert(error.toString()); }); } } @@ -324,7 +324,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( }); this.model.moveBank(from, to) .catch((error) => { - this.model.showAlert(error); + this.model.showAlert(error.toString()); }); } diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index 9105244..b2ca5d2 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -349,7 +349,7 @@ export default withStyles(styles, { withTheme: true })( } ).catch( (e: any) => { - this.model.showAlert(e + ""); + this.model.showAlert(e.toString()); } ); } diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index fd3e455..8d5cfcc 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -20,7 +20,7 @@ import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin'; import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError'; -import { UpdateStatus, UpdatePolicyT} from './Updater'; +import { UpdateStatus, UpdatePolicyT } from './Updater'; import { ObservableProperty } from './ObservableProperty'; import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard' import PluginClass from './PluginClass'; @@ -54,8 +54,7 @@ export enum State { InstallingUpdate, }; -export function wantsLoadingScreen(state: State) -{ +export function wantsLoadingScreen(state: State) { return state >= State.Reconnecting; } @@ -442,8 +441,7 @@ export class PiPedalModel //implements PiPedalModel this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this); } - expectDisconnect(reason: ReconnectReason) - { + expectDisconnect(reason: ReconnectReason) { this.reconnectReason = reason; } @@ -647,38 +645,36 @@ export class PiPedalModel //implements PiPedalModel private updateLaterTimeout?: NodeJS.Timeout = undefined; - private clearPromptForUpdateTimer() - { - if (this.updateLaterTimeout) - { + private clearPromptForUpdateTimer() { + if (this.updateLaterTimeout) { clearTimeout(this.updateLaterTimeout); this.updateLaterTimeout = undefined; } } - private setPromptForUpdateTimer(when: Date) - { - let ms = when.getTime()-Date.now(); + 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)=>{ + .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; @@ -689,36 +685,36 @@ export class PiPedalModel //implements PiPedalModel let nDate = Date.now(); let now: Date = new Date(nDate); - let tomorrow: Date = new Date(nDate + 86400000); + let maxDate: Date = new Date(nDate + 86400000*2); // sanity check for systems with unstable system clock - timeEnabled = (updateLaterTime < now || updateLaterTime >= tomorrow) - if (!timeEnabled) - { - this.setPromptForUpdateTimer(updateLaterTime); - } + timeEnabled = (updateLaterTime < now || updateLaterTime >= maxDate) } let updateStatus = this.updateStatus.get(); let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable; - if (stateEnabled && timeEnabled && statusEnabled) + let canUpdateNow: boolean = (stateEnabled && timeEnabled && statusEnabled); + if (updateStatus.updatePolicy === UpdatePolicyT.Disable) { - this.showUpdateDialogValue = true; + canUpdateNow = false; } - this.promptForUpdate.set(this.showUpdateDialogValue); - if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime) + 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) - { + if (this.showUpdateDialogValue !== show) { this.showUpdateDialogValue = show; - if (show) - { + if (show) { this.forceUpdateCheck(); } this.updatePromptForUpdate(); @@ -732,6 +728,7 @@ export class PiPedalModel //implements PiPedalModel 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); @@ -755,7 +752,6 @@ export class PiPedalModel //implements PiPedalModel setState(state: State) { if (this.state.get() !== state) { this.state.set(state); - this.updatePromptForUpdate(); } } @@ -809,7 +805,7 @@ export class PiPedalModel //implements PiPedalModel return this.getUpdateStatus(); // detects whether server has been upgraded. }) .then((updateStatus) => { - + return this.getWebSocket().request("plugins"); }) .then(data => { @@ -884,7 +880,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 { @@ -971,7 +967,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) => { @@ -1066,7 +1062,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; }); @@ -1150,7 +1146,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; @@ -1789,28 +1785,31 @@ export class PiPedalModel //implements PiPedalModel updateNow(): Promise { return new Promise( - (accept,reject) => { + (accept, reject) => { let updateStatus = this.updateStatus.get(); - if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable) - { + if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable) { this.setState(State.DownloadingUpdate); + this.expectDisconnect(ReconnectReason.Updating); let url = updateStatus.getActiveRelease().updateUrl; + nullCast(this.webSocket) .request('updateNow', url) - .then(()=> { + .then(() => { this.setState(State.InstallingUpdate); accept(); }) .catch( - (e: any)=>{ + (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.")); } - - } + + } ); } @@ -1896,8 +1895,9 @@ 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); } @@ -2009,8 +2009,7 @@ export class PiPedalModel //implements PiPedalModel } private isClosed = false; close() { - if (!this.isClosed) - { + if (!this.isClosed) { this.isClosed = true; this.webSocket?.close(); this.webSocket = undefined; @@ -2385,7 +2384,7 @@ export class PiPedalModel //implements PiPedalModel resolve(json as number); }) .catch((error) => { - reject("Upload failed. " + error); + reject("Upload failed. " + error.toString()); }) ; } catch (error) { @@ -2775,7 +2774,7 @@ export class PiPedalModel //implements PiPedalModel } - setUpdatePolicy(updatePolicy: UpdatePolicyT) : void { + setUpdatePolicy(updatePolicy: UpdatePolicyT): void { let iPolicy = updatePolicy as number; if (this.webSocket) { this.webSocket.send("setUpdatePolicy", iPolicy); diff --git a/react/src/UpdateDialog.tsx b/react/src/UpdateDialog.tsx index aee7a05..c61269e 100644 --- a/react/src/UpdateDialog.tsx +++ b/react/src/UpdateDialog.tsx @@ -30,15 +30,14 @@ 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 { 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 -const CLOSE_DELAY = 100; // ms. +const UPDATE_CHECK_DELAY = 86400000; // one day in ms. +// const UPDATE_CHECK_DELAY = 30 * 1000; // testing @@ -69,11 +68,9 @@ export default class UpdateDialog extends React.Component { this.handleClose(); }} - style={{ userSelect: "none" }} + style={{ userSelect: "none" }} > { (!compact) && @@ -163,10 +163,11 @@ export default class UpdateDialog extends React.Component
PiPedal Updates - { this.onPolicySelected(ev.target.value); }}> + Release only + Release or Beta + Development + Disable
@@ -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