diff --git a/.vscode/launch.json b/.vscode/launch.json index f75aadd..3e7d49c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "Attach to Edge", "port": 9222, @@ -148,7 +149,13 @@ "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true + }, + { + "description": "Add alsa source directories.", + "text": "directory $cd:$cwd:/usr/src/alsa-lib-1.2.8", + "ignoreFailures": true } + ] }, diff --git a/CMakeLists.txt b/CMakeLists.txt index 64bdd88..06754e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set (CMAKE_INSTALL_PREFIX "/usr/") include(CTest) enable_testing() -add_subdirectory("submodules/pipedal_p2pd") +#add_subdirectory("submodules/pipedal_p2pd") add_subdirectory("PiPedalCommon") @@ -82,8 +82,9 @@ install(CODE EXECUTABLES ${CMAKE_INSTALL_PREFIX}/sbin/pipedaladmind ${CMAKE_INSTALL_PREFIX}/sbin/pipedald - ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_nm_p2pd - ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_p2pd + ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_update + #${CMAKE_INSTALL_PREFIX}/sbin/pipedal_nm_p2pd + #${CMAKE_INSTALL_PREFIX}/sbin/pipedal_p2pd ${CMAKE_INSTALL_PREFIX}/bin/pipedalconfig ) ]] diff --git a/PiPedalCommon/src/DBusDispatcher.cpp b/PiPedalCommon/src/DBusDispatcher.cpp index a9ad904..234efdf 100644 --- a/PiPedalCommon/src/DBusDispatcher.cpp +++ b/PiPedalCommon/src/DBusDispatcher.cpp @@ -6,6 +6,7 @@ #include "ss.hpp" #include #include +#include "util.hpp" DBusDispatcher::DBusDispatcher(bool systemBus) { @@ -87,6 +88,7 @@ void DBusDispatcher::WakeThread() } void DBusDispatcher::ThreadProc() { + pipedal::SetThreadName("dbusd"); try { diff --git a/PiPedalCommon/src/WifiConfigSettings.cpp b/PiPedalCommon/src/WifiConfigSettings.cpp index f0249f5..399a6a6 100644 --- a/PiPedalCommon/src/WifiConfigSettings.cpp +++ b/PiPedalCommon/src/WifiConfigSettings.cpp @@ -30,7 +30,7 @@ #include #include #include -#include // for gethostname() +#include "util.hpp" using namespace pipedal; using namespace std; @@ -89,16 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text) return std::isalpha(text[0]) && std::isalpha(text[1]); } -static std::string GetHostName() -{ - char buffer[1024]; - if (gethostname(buffer,1024) != 0) - { - buffer[0] = '\0'; - } - buffer[1023] = '\0'; - return buffer; -} void WifiConfigSettings::Load() { diff --git a/PiPedalCommon/src/include/Lv2Log.hpp b/PiPedalCommon/src/include/Lv2Log.hpp index 721c1a5..d04c6f1 100644 --- a/PiPedalCommon/src/include/Lv2Log.hpp +++ b/PiPedalCommon/src/include/Lv2Log.hpp @@ -150,7 +150,7 @@ namespace pipedal static void error(const char *format, ...) { - if (Lv2Log::log_level_ >= LogLevel::Warning) + if (Lv2Log::log_level_ >= LogLevel::Error) { va_list arglist; va_start(arglist, format); diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 9a3fa3c..93e6343 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -35,4 +35,7 @@ namespace pipedal { std::u32string ToUtf32(const std::string &s); inline bool isPathSeperator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator;} + + std::string GetHostName(); + } diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index 4247a7b..6fbc1a9 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -100,3 +100,15 @@ std::u32string pipedal::ToUtf32(const std::string &s) } return result.str(); } + + +std::string pipedal::GetHostName() +{ + char buffer[1024]; + if (gethostname(buffer,1024) != 0) + { + buffer[0] = '\0'; + } + buffer[1023] = '\0'; + return buffer; +} diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so index 1308d68..ca752be 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index 8c23ed6..94f866d 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/react/src/AboutDialog.tsx b/react/src/AboutDialog.tsx index db3120e..8a65e02 100644 --- a/react/src/AboutDialog.tsx +++ b/react/src/AboutDialog.tsx @@ -6,7 +6,7 @@ import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Divider from '@mui/material/Divider'; -import ArrowBackIcon from '@mui/icons-material//ArrowBack'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import JackHostStatus from './JackHostStatus'; import { PiPedalError } from './PiPedalError'; @@ -16,12 +16,9 @@ import { Theme, createStyles } from '@mui/material/styles'; import { withStyles, WithStyles } from '@mui/styles'; import Slide, { SlideProps } from '@mui/material/Slide'; - interface AboutDialogProps extends WithStyles { open: boolean; onClose: () => void; - - }; interface AboutDialogState { @@ -76,6 +73,15 @@ const styles = (theme: Theme) => createStyles({ }, secondaryItem: { + }, + can_select: { + userSelect: "text", + cursor: "text" + }, + no_select: { + userSelect: "none", + cursor: "text" + } @@ -173,7 +179,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( this.updateNotifications(); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void { - super.componentDidUpdate?.(prevProps,prevState,snapshot); + super.componentDidUpdate?.(prevProps, prevState, snapshot); this.updateNotifications(); } @@ -185,6 +191,11 @@ const AboutDialog = withStyles(styles, { withTheme: true })( render() { let classes = this.props.classes; let addressKey = 0; + let serverVersion = this.model.serverVersion?.serverVersion ?? ""; + let nPos = serverVersion.indexOf(' '); + if (nPos !== -1) { + serverVersion = serverVersion.substring(nPos + 1); + } return ( { this.props.onClose() }} TransitionComponent={Transition} @@ -210,13 +221,16 @@ const AboutDialog = withStyles(styles, { withTheme: true })( overflowX: "hidden", overflowY: "auto", userSelect: "text" }} > -
- - PiPedal - {(this.model.serverVersion ? this.model.serverVersion.serverVersion : "") - + (this.model.serverVersion?.debug ? " (Debug)" : "")} - - +
+
+ + PiPedal + {serverVersion + + (this.model.serverVersion?.debug ? " (Debug)" : "")} + + + +
Copyright © 2022-2024 Robin Davies. @@ -235,24 +249,34 @@ const AboutDialog = withStyles(styles, { withTheme: true })( ADDRESSES -
- { - this.model.serverVersion?.webAddresses.map((address) => - ( - - {address} - - )) +
+ { + this.model.serverVersion?.webAddresses.map((address) => + ( + + {address} + + )) - } + }
+ + + SERVER OS + +
+ + {this.model.serverVersion?.osVersion ?? ""} + +
+
LEGAL NOTICES -
+
diff --git a/react/src/App.tsx b/react/src/App.tsx index dc0bb9d..8e9637a 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -59,11 +59,13 @@ const theme = createTheme( styleOverrides: { containedPrimary: { borderRadius: '9999px', - paddingLeft: "14px", paddingRight: "16px" + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" }, containedSecondary: { borderRadius: '9999px', - paddingLeft: "14px", paddingRight: "16px" + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" } }, @@ -103,11 +105,13 @@ const theme = createTheme( styleOverrides: { containedPrimary: { borderRadius: '9999px', - paddingLeft: "14px", paddingRight: "16px" + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" }, containedSecondary: { borderRadius: '9999px', - paddingLeft: "14px", paddingRight: "16px" + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" } }, diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index 9537c06..9a05c74 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog'; import AboutDialog from './AboutDialog'; import BankDialog from './BankDialog'; -import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel'; +import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo, wantsLoadingScreen } from './PiPedalModel'; import ZoomedUiControl from './ZoomedUiControl' import MainPage from './MainPage'; import DialogContent from '@mui/material/DialogContent'; @@ -67,7 +67,7 @@ import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg'; import { ReactComponent as EditBanksIcon } from './svg/ic_edit_banks.svg'; import { ReactComponent as SettingsIcon } from './svg/ic_settings.svg'; import { ReactComponent as HelpOutlineIcon } from './svg/ic_help_outline.svg'; -import DialogEx from './DialogEx'; +import DialogEx, { DialogStackState } from './DialogEx'; const appStyles = (theme: Theme) => createStyles({ @@ -139,10 +139,11 @@ const appStyles = (theme: Theme) => createStyles({ marginTop: 0, fontWeight: 500, fontSize: "13pt", - maxWidth: 350, opacity: 1, zIndex: 2010, paddingTop: 12, + gravity: "center", + textAlign: "center" }, errorMessageBox: { @@ -164,8 +165,6 @@ const appStyles = (theme: Theme) => createStyles({ position: "relative", top: "20%", color: isDarkMode() ? theme.palette.text.secondary : "#888", - marginLeft: "auto", - marginRight: "auto", // border: "3px solid #888", borderRadius: "12px", padding: "12px", @@ -351,9 +350,40 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.handleCloseAlert = this.handleCloseAlert.bind(this); this.banksChangedHandler = this.banksChangedHandler.bind(this); this.showStatusMonitorHandler = this.showStatusMonitorHandler.bind(this); - + this.onPopStateHandler = this.onPopStateHandler.bind(this); } + showDrawer() { + if (!this.state.isDrawerOpen) { + this.setState({ isDrawerOpen: true }) + this.pushOpenMenuState(); + } + } + hideDrawer(loadingDialog: boolean = false) { + if (!loadingDialog && window.location.hash === "#menu") { + window.history.back(); + } else { + this.setState({ isDrawerOpen: false }) + } + } + + pushOpenMenuState() { + let stackState = { tag: "OpenMenu", previousState: null }; + window.history.replaceState(stackState, "", window.location.href); + window.history.pushState({}, "", "#menu"); + } + onPopStateHandler(ev: PopStateEvent) { + let stackState: DialogStackState | null = ev.state as (DialogStackState | null); + if (stackState && stackState.tag === "OpenMenu") { + if (this.state.isDrawerOpen) { + this.setState({ isDrawerOpen: false }); + } + ev.stopPropagation(); + window.history.replaceState(stackState.previousState, "", window.location.href); + return true; + } + return false; + } onOpenBank(bankId: number) { this.model_.openBank(bankId) @@ -524,11 +554,9 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< return undefined; } promptForUpdateHandler(newValue: boolean) { - if (this.model_.enableAutoUpdate) - { - if (this.state.updateDialogOpen !== newValue) - { - this.setState({updateDialogOpen: newValue}); + if (this.model_.enableAutoUpdate) { + if (this.state.updateDialogOpen !== newValue) { + this.setState({ updateDialogOpen: newValue }); } } } @@ -537,6 +565,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< super.componentDidMount(); window.addEventListener("beforeunload", this.beforeUnloadListener); window.addEventListener("unload", this.unloadListener); + window.addEventListener("popstate", this.onPopStateHandler); this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_); this.model_.state.addOnChangedHandler(this.stateChangeHandler_); @@ -546,6 +575,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler); this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler); this.alertMessageChangedHandler(); + } @@ -568,6 +598,9 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< super.componentWillUnmount(); window.removeEventListener("beforeunload", this.beforeUnloadListener); + window.removeEventListener("popstate", this.onPopStateHandler); + + this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler); this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_); this.model_.state.removeOnChangedHandler(this.stateChangeHandler_); @@ -634,12 +667,6 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< } } - showDrawer() { - this.setState({ isDrawerOpen: true }) - } - hideDrawer() { - this.setState({ isDrawerOpen: false }) - } shortBankList(banks: BankIndex): BankIndexEntry[] { let n = this.state.bankDisplayItems; let entries = banks.entries; @@ -796,7 +823,11 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< { showBankSelectDialog && ( this.handleDrawerSelectBank()} + onClick={(ev) => { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSelectBank(); + }} > @@ -808,19 +839,34 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< - { this.handleDrawerRenameBank() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerRenameBank() + }}> - { this.handleDrawerSaveBankAs() }} > + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSaveBankAs(); + }} > - { this.handleDrawerManageBanks(); }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerManageBanks(); + }}> @@ -829,19 +875,34 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< - { this.handleDrawerSettingsClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSettingsClick() + }}> - { this.handleDrawerAboutClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerAboutClick(); + }}> - { this.handleDrawerDonationClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerDonationClick(); + }}> @@ -865,10 +926,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
this.setState({ bankDialogOpen: false })} /> - { (this.state.aboutDialogOpen)&& - ( - this.setState({ aboutDialogOpen: false })} /> - )} + {(this.state.aboutDialogOpen) && + ( + this.setState({ aboutDialogOpen: false })} /> + )} @@ -941,7 +1002,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
- + {this.getReloadingMessage()}
@@ -961,7 +1022,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<

- diff --git a/react/src/DialogEx.tsx b/react/src/DialogEx.tsx index 3288327..a13e0ab 100644 --- a/react/src/DialogEx.tsx +++ b/react/src/DialogEx.tsx @@ -31,21 +31,17 @@ interface DialogExState { } class DialogStackEntry { - constructor(tag: string) + constructor(dialog: DialogEx) { - this.tag = tag; + this.tag = dialog.props.tag; + this.dialog = dialog; } tag: string; + dialog: DialogEx; }; let dialogStack: DialogStackEntry[] = []; -function peekDialogStack(): string { - if (dialogStack.length !== 0) { - return dialogStack[dialogStack.length-1].tag; - } - return ""; -} function popDialogStack(): void { if (dialogStack.length !== 0) @@ -53,12 +49,33 @@ function popDialogStack(): void { dialogStack.splice(dialogStack.length-1,1); } } -function pushDialogStack(tag: string): void { - dialogStack.push(new DialogStackEntry(tag)); +function pushDialogStack(dialog: DialogEx): void { + dialogStack.push(new DialogStackEntry(dialog)); } +export interface DialogStackState{ + tag: String; + previousState: DialogStackState | null; +}; +// Close all dialogs higher in the dialog stack than "tag". +export function removeDialogStackEntriesAbove(tag: string) +{ + for (let i = dialogStack.length-1; i >= 0; --i) + { + let entry = dialogStack[i]; + if (entry.tag === tag) + { + return; + } + popDialogStack(); + if (entry.dialog.props.open && entry.dialog.props.onClose) + { + entry.dialog.props.onClose({}, "backdropClick"); + } + } +} class DialogEx extends React.Component { constructor(props: DialogExProps) { @@ -75,22 +92,28 @@ class DialogEx extends React.Component { hasHooks: boolean = false; stateWasPopped: boolean = false; - handlePopState(e: any): any + handlePopState(ev: PopStateEvent): any { - let shouldClose = (peekDialogStack() === this.props.tag); - if (shouldClose) - { + let evTag: DialogStackState | null = ev.state as DialogStackState | null; + if (evTag && evTag.tag === this.props.tag) { + removeDialogStackEntriesAbove(this.props.tag); if (!this.stateWasPopped) { this.stateWasPopped = true; popDialogStack(); if (this.props.open) { - this.props.onClose?.(e,"backdropClick"); + this.props.onClose?.(ev,"backdropClick"); } } + window.history.replaceState(evTag.previousState,"",window.location.href); window.removeEventListener("popstate",this.handlePopState); - e.stopPropagation(); + + if (window.location.hash === "#menu") // The menu popstate is next? + { + window.history.back(); // then clear off the menu popstate too. + } + ev.stopPropagation(); } } @@ -105,19 +128,18 @@ class DialogEx extends React.Component { this.stateWasPopped = false; window.addEventListener("popstate",this.handlePopState); - pushDialogStack(this.props.tag); - let state: {tag: string} = {tag: this.props.tag}; - // eslint-disable-next-line no-restricted-globals - history.pushState( - state, + pushDialogStack(this); + let dialogStackState: DialogStackState = {tag: this.props.tag,previousState: window.history.state as DialogStackState | null}; + window.history.replaceState(dialogStackState,"",window.location.href); + window.history.pushState( + null, "", "#" + this.props.tag ); } else { if (!this.stateWasPopped) { - // eslint-disable-next-line no-restricted-globals - history.back(); + window.history.back(); } } } diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 46d9cfa..0593afe 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -793,17 +793,47 @@ export class PiPedalModel //implements PiPedalModel return this.webSocket; } + androidReconnectTimeout?: NodeJS.Timeout = undefined; + + cancelAndroidReconnectTimer() + { + if (this.androidReconnectTimeout) { + clearTimeout(this.androidReconnectTimeout); + this.androidReconnectTimeout = undefined; + } + } + + startAndroidReconnectTimer() + { + this.cancelAndroidReconnectTimer(); + this.androidReconnectTimeout = setTimeout(()=>{ + this.androidReconnectTimeout = undefined; + this.androidHost?.setDisconnected(true); + },20*1000); + } onSocketConnectionLost() { // remove all the events and subscriptions we have. + if (this.isClosed) + { + return; // page unloading. do NOT change the UI. + } this.vuSubscriptions = []; this.monitorPatchPropertyListeners = []; if (this.isAndroidHosted()) { - this.androidHost?.setDisconnected(true); + // if unexpected, go back to the device browser immediately. + if (this.reconnectReason === ReconnectReason.Disconnected) + { + this.androidHost?.setDisconnected(true); + } else { + this.startAndroidReconnectTimer(); + } } } onSocketReconnected() { this.cancelOnNetworkChanging(); + this.cancelAndroidReconnectTimer(); + if (this.isAndroidHosted()) { this.androidHost?.setDisconnected(false); } diff --git a/react/src/PiPedalSocket.tsx b/react/src/PiPedalSocket.tsx index 3cdc6ca..49315d7 100644 --- a/react/src/PiPedalSocket.tsx +++ b/react/src/PiPedalSocket.tsx @@ -278,7 +278,7 @@ class PiPedalSocket { ws.onerror = null; reject("Connection closed unexpectedly."); }; - ws.onerror = (evWheent: Event) => { + ws.onerror = (evt: Event) => { ws.onclose = null; ws.onerror = null; reject("Failed to connect."); diff --git a/react/src/WifiConfigDialog.tsx b/react/src/WifiConfigDialog.tsx index 2051b62..1affb61 100644 --- a/react/src/WifiConfigDialog.tsx +++ b/react/src/WifiConfigDialog.tsx @@ -451,7 +451,6 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })( InputLabelProps={{ shrink: true }} - disabled={!enabled} />
false}> {/*Prevents chrome from saving passwords */} @@ -477,7 +476,9 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })( }} InputProps={{ startAdornment: - this.props.wifiConfigSettings.hasSavedPassword && !this.state.hasPassword && !this.state.showPassword ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : "" + !this.state.hasPassword && !this.state.showPassword ? + (this.props.wifiConfigSettings.hasSavedPassword? "(Unchanged)" : "(Required)") + : "" , endAdornment: ( terminateAudio_ = false; void terminateAudio(bool terminate) { - std::lock_guard lock{terminateSync}; this->terminateAudio_ = terminate; } bool terminateAudio() { - std::lock_guard lock{terminateSync}; return this->terminateAudio_; } @@ -1297,10 +1295,10 @@ namespace pipedal FillOutputBuffer(); err = snd_pcm_start(handle); - // if (err < 0) - // { - // throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); - // } + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + } return; } else if (err == -ESTRPIPE) @@ -1320,7 +1318,7 @@ namespace pipedal } return; } - throw PiPedalStateException(SS("ALSA error:" << snd_strerror(err))); + throw PiPedalStateException(SS("ALSA error: " << snd_strerror(err))); } std::jthread *audioThread = nullptr; @@ -1663,16 +1661,16 @@ namespace pipedal } void Close() { - if (hInParams) - { - snd_rawmidi_params_free(hInParams); - hInParams = 0; - } if (hIn) { snd_rawmidi_close(hIn); hIn = nullptr; } + if (hInParams) + { + snd_rawmidi_params_free(hInParams); + hInParams = 0; + } } int GetDataLength(uint8_t cc) diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index d248fae..9547e62 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -349,17 +349,14 @@ private: virtual void Close() { - std::lock_guard guard{mutex}; - if (!isOpen) - return; - - isOpen = false; - - if (realtimeMonitorPortSubscriptions != nullptr) { - delete realtimeMonitorPortSubscriptions; - realtimeMonitorPortSubscriptions = nullptr; + std::lock_guard guard{mutex}; + if (!isOpen) + return; + + isOpen = false; } + if (active) { audioDriver->Deactivate(); @@ -371,6 +368,7 @@ private: StopReaderThread(); + // release any pdealboards owned by the process thread. this->activePedalboards.resize(0); this->realtimeActivePedalboard = nullptr; @@ -815,7 +813,7 @@ private: virtual void OnAudioTerminated() override { this->active = false; - Lv2Log::info("Audio stopped."); + Lv2Log::info("Audio thread terminated."); } diff --git a/src/AvahiService.cpp b/src/AvahiService.cpp index 026445a..02f54cc 100644 --- a/src/AvahiService.cpp +++ b/src/AvahiService.cpp @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2022 Robin E. R. Davies - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -24,6 +24,7 @@ #include "AvahiService.hpp" #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include "Lv2Log.hpp" #include "ss.hpp" +#include using namespace pipedal; @@ -43,43 +45,122 @@ void AvahiService::Announce( const std::string &name, const std::string &instanceId, const std::string &mdnsName, - bool addTestGroup) + bool wait) { + if (terminated) + return; // We should update an existing group if the name doesn't change. - // but in practice, the name is the only thing that's going to change without restarting the server. - if (this->name && strcmp(name.c_str(),this->name) == 0) + // but in practice, the name is the only thing that's going to change without restarting the pipedal server. + if (this->serviceName == name) { return; } - Unannounce(); - this->portNumber = portNumber; - this->instanceId = instanceId; - this->mdnsName = mdnsName; - this->addTestGroup = addTestGroup; - - this->name = avahi_strdup(name.c_str()); - - Start(); -} - -void AvahiService::Unannounce() -{ - Stop(); - if (name) + // this->name = avahi_strdup(name.c_str()); + if (!started) { - avahi_free(name); - this->name = nullptr; + this->portNumber = portNumber; + this->instanceId = instanceId; + this->mdnsName = mdnsName; + this->serviceName = name; + + started = true; + makeAnnouncement = true; + createPending = true; + Start(); + if (wait) { + Wait(); + } + } + else + { + // already started, so we have to use poll_lock instead. + avahi_threaded_poll_lock(threadedPoll); + + // all state that we have to protect with the lock now that the avahi serv3ce is running. + this->portNumber = portNumber; + this->instanceId = instanceId; + this->mdnsName = mdnsName; + this->serviceName = name; + this->makeAnnouncement = true; + + // we've requested a start but have not created the group. + if (this->createPending) + { + // the first create will use our freshly updated parameters. + // no action required. + } + else + { + // otherwise we have to use a lock to effect the update. + if (group) + { + avahi_entry_group_reset(group); // unannounce the previous + SetState(ServiceState::Reset); + create_group(client); + } + else + { + Lv2Log::error("Failed to update mDNS service because of a synch problem."); + } + } + avahi_threaded_poll_unlock(threadedPoll); + if (wait) + { + Wait(); + } } - this->group = nullptr; - this->client = nullptr; - this->threadedPoll = nullptr; } +void AvahiService::Unannounce(bool wait) +{ + if (terminated) + return; + if (this->threadedPoll) + { + avahi_threaded_poll_lock(threadedPoll); + if (started) + { + if (this->createPending) + { + // if service is starting up, and we haven't yet made our first announcement, + // just signal that we don't want to announc. + this->makeAnnouncement = false; + } + else + { + this->makeAnnouncement = false; + // we have previously made a successful announcement. Retract it. + if (group) + { + int rc = avahi_entry_group_reset(group); + if (rc < 0) + { + Lv2Log::error("Avahi: failed to un-announce."); + } + } + } + } + avahi_threaded_poll_unlock(threadedPoll); + if (wait) + { + WaitForUnannounce(); + } + } +} + + + void AvahiService::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userData) { ((AvahiService *)userData)->EntryGroupCallback(g, state); } + +void AvahiService::SetState(ServiceState serviceState) +{ + this->serviceState = serviceState; +} + void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState state) { group = g; @@ -87,29 +168,39 @@ void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState s switch (state) { case AVAHI_ENTRY_GROUP_ESTABLISHED: + SetState(ServiceState::Established); + Lv2Log::info(SS("DNS/SD group established.")); + /* The entry group has been established successfully */ - Lv2Log::debug(SS("Service " << name << " successfully established.")); break; case AVAHI_ENTRY_GROUP_COLLISION: { - char *n; /* A service name collision with a remote service - * happened. Let's pick a new name */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; - Lv2Log::error(SS("Service name collision, renaming service to '" << name << "'\n")); + * happened. Let's pick a new name */ + SetState(ServiceState::Collision); + char *n = avahi_alternative_service_name(avahiNameString); + avahi_free(avahiNameString); + avahiNameString = n; + Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'\n")); /* And recreate the services */ create_group(avahi_entry_group_get_client(g)); break; } case AVAHI_ENTRY_GROUP_FAILURE: Lv2Log::error(SS("Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) << "\n")); + Lv2Log::error(SS("DNS/SD service shutting down.")); /* Some kind of failure happened while we were registering our services */ + terminated = true; avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); break; case AVAHI_ENTRY_GROUP_UNCOMMITED: + SetState(ServiceState::Uncommited); + break; case AVAHI_ENTRY_GROUP_REGISTERING:; + SetState(ServiceState::Registering); + break; + } } @@ -142,11 +233,14 @@ static int toRawDns(char *rawResult, size_t size, const std::string &name) } void AvahiService::create_group(AvahiClient *c) { + this->createPending = false; + char *n; int ret; assert(c); /* If this is the first time we're called, let's create a new * entry group if necessary */ + SetState(ServiceState::Requested); if (!group) { if (!(group = avahi_entry_group_new(c, entry_group_callback, (void *)this))) @@ -157,20 +251,26 @@ void AvahiService::create_group(AvahiClient *c) } /* If the group is empty (either because it was just created, or * because it was reset previously, add our entries. */ - if (avahi_entry_group_is_empty(group)) + if (this->makeAnnouncement && avahi_entry_group_is_empty(group)) { - Lv2Log::debug(SS("Adding service '" << name << "'")); + Lv2Log::debug(SS("Adding service '" << avahiNameString << "'")); std::string instanceTxtRecord = SS("id=" << this->instanceId); #define PIPEDAL_SERVICE_TYPE "_pipedal._tcp" + if (avahiNameString) + { + avahi_free(avahiNameString); + avahiNameString = nullptr; + } + avahiNameString = avahi_strdup(serviceName.c_str()); if ((ret = avahi_entry_group_add_service( group, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.) (AvahiPublishFlags)0, - name, + avahiNameString, PIPEDAL_SERVICE_TYPE, NULL, NULL, @@ -186,55 +286,31 @@ void AvahiService::create_group(AvahiClient *c) goto fail; } - if (this->addTestGroup) - { - Lv2Log::info("Added tests DNS/SD service."); - std::string instanceTxtRecord = SS("id=" << "0a6045b0-1753-4104-b3e4-b9713b9cc360"); - - if ((ret = avahi_entry_group_add_service( - group, - AVAHI_IF_UNSPEC, - AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.) - (AvahiPublishFlags)0, - "Ed's PiPedal", - PIPEDAL_SERVICE_TYPE, - NULL, - NULL, - portNumber, - - // txt records. - instanceTxtRecord.c_str(), - NULL)) < 0) - { - if (ret == AVAHI_ERR_COLLISION) - goto collision; - Lv2Log::error(SS("Failed to add _pipedal._tcp service: " << avahi_strerror(ret))); - goto fail; - } - } - /* Tell the server to register the service */ if ((ret = avahi_entry_group_commit(group)) < 0) { Lv2Log::error(SS("Failed to commit entry group: " << avahi_strerror(ret))); goto fail; } - Lv2Log::info(SS("DNS/SD service announced.")); - + Lv2Log::info(SS("DNS/SD service announced. (" << this->avahiNameString << ")")); } return; collision: /* A service name collision with a local service happened. Let's - * pick a new name */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; - Lv2Log::warning(SS("Service name collision, renaming service to '" << name << "'")); + * pick a new avahiNameString */ + n = avahi_alternative_service_name(avahiNameString); + avahi_free(avahiNameString); + avahiNameString = n; + Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'")); avahi_entry_group_reset(group); create_group(c); return; fail: + terminated = true; avahi_threaded_poll_quit(threadedPoll); + Lv2Log::error("DNS/SD service shutting down."); + SetState(ServiceState::Failed); + } void AvahiService::client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *userdata) @@ -250,7 +326,7 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) { case AVAHI_CLIENT_S_RUNNING: /* The server has startup successfully and registered its host - * name on the network, so it's time to create our services */ + * name on the network, so it's time to create our services */ create_group(c); break; case AVAHI_CLIENT_FAILURE: @@ -259,11 +335,11 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) if (this->clientErrno == AVAHI_ERR_DISCONNECTED) { // tear the client down and restart it - Lv2Log::info("Avahi connection lost. Reconnecting."); + Lv2Log::info("DNS/SD connection lost. Reconnecting."); if (group) { - avahi_entry_group_reset(group); + avahi_entry_group_free(group); group = nullptr; } @@ -278,28 +354,35 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) if (!client) { Lv2Log::error(SS("Failed to create client: " << avahi_strerror(error))); + terminated = true; avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); } } else { - Lv2Log::error(SS("Client failure: " << avahi_strerror(avahi_client_errno(c)))); + terminated = true; + Lv2Log::error(SS("DNS/SD client failure: " << avahi_strerror(avahi_client_errno(c)))); avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); } break; case AVAHI_CLIENT_S_COLLISION: /* Let's drop our registered services. When the server is back - * in AVAHI_SERVER_RUNNING state we will register them - * again with the new host name. */ + * in AVAHI_SERVER_RUNNING state we will register them + * again with the new host name. */ case AVAHI_CLIENT_S_REGISTERING: /* The server records are now being established. This - * might be caused by a host name change. We need to wait - * for our own records to register until the host name is - * properly esatblished. */ + * might be caused by a host name change. We need to wait + * for our own records to register until the host name is + * properly esatblished. */ if (group) avahi_entry_group_reset(group); + SetState(ServiceState::Settling); + break; + case AVAHI_CLIENT_CONNECTING: + SetState(ServiceState::Settling); break; - case AVAHI_CLIENT_CONNECTING:; } } @@ -310,7 +393,8 @@ void AvahiService::Start() int ret = 1; struct timeval tv; this->clientErrno = 0; - while (true) + SetState(ServiceState::Initializing); + for (int retry = 0; retry < 3; ++retry) { /* Allocate main loop object */ if (!this->threadedPoll) @@ -339,30 +423,129 @@ void AvahiService::Start() return; fail: Stop(); + sleep(1); + SetState(ServiceState::Failed); } return; } void AvahiService::Stop() { + if (stopped) + return; + this->stopped = true; - /* Cleanup things */ - if (group) - { - avahi_entry_group_reset(group); - avahi_entry_group_free(group); - group = nullptr; - } - if (client) - { - avahi_client_free(client); - client = nullptr; - } + Unannounce(true); + sleep(1); // let traffic setting. avahi doens't seem to shut down cleany otherwise. if (threadedPoll) { + avahi_threaded_poll_lock(threadedPoll); + if (group) + { + avahi_entry_group_free(group); + group = nullptr; + } + avahi_threaded_poll_unlock(threadedPoll); + avahi_threaded_poll_lock(threadedPoll); + if (client) + { + avahi_client_free(client); + client = nullptr; + } + avahi_threaded_poll_unlock(threadedPoll); + avahi_threaded_poll_stop(threadedPoll); avahi_threaded_poll_free(threadedPoll); + SetState(ServiceState::Closed); threadedPoll = nullptr; } + + /* Cleanup things */ + if (avahiNameString) + { + avahi_free(avahiNameString); + avahiNameString = nullptr; + } + Lv2Log::info("DNS/SD service stopped."); } + +void AvahiService::Wait() +{ + using clock = std::chrono::steady_clock; + auto start = clock::now(); + auto waitTime = start + std::chrono::duration_cast(std::chrono::seconds(5)); + while (true) + { + bool done; + switch (serviceState) + { + default: + case ServiceState::Unitialized: + throw std::runtime_error("Invalid state"); + + case ServiceState::Initializing: + case ServiceState::Settling: + case ServiceState::Requested: + case ServiceState::Uncommited: + case ServiceState::Registering: + case ServiceState::Collision: + done = false; + break; + case ServiceState::Reset: + case ServiceState::Established: + case ServiceState::Failed: + case ServiceState::Closed: + done = true; + break; + }; + if (done) break; + if (clock::now() > waitTime) + { + Lv2Log::error("DNS/SD announcement timed out."); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } +} +void AvahiService::WaitForUnannounce() +{ + using clock = std::chrono::steady_clock; + auto start = clock::now(); + auto waitTime = start + std::chrono::duration_cast(std::chrono::seconds(5)); + + while (true) + { + bool done; + switch (serviceState) + { + default: + throw std::runtime_error("Invalid state"); + + case ServiceState::Initializing: + case ServiceState::Settling: + case ServiceState::Requested: + case ServiceState::Registering: + case ServiceState::Collision: + done = false; + break; + case ServiceState::Uncommited: + case ServiceState::Unitialized: + case ServiceState::Reset: + case ServiceState::Established: + case ServiceState::Failed: + case ServiceState::Closed: + done = true; + break; + }; + if (done) break; + if (clock::now() > waitTime) + { + Lv2Log::error("DNS/SD unannounce timed out."); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + +} + diff --git a/src/AvahiService.hpp b/src/AvahiService.hpp index 60dafff..ce7f607 100644 --- a/src/AvahiService.hpp +++ b/src/AvahiService.hpp @@ -26,10 +26,12 @@ #include #include +#include // forward declarations. class AvahiEntryGroup; class AvahiThreadedPoll; +#include #include @@ -38,12 +40,37 @@ namespace pipedal { class AvahiService { public: - ~AvahiService() { Unannounce(); } + ~AvahiService() { Stop(); } void Announce( - int portNumber, const std::string &name, const std::string &instanceId, const std::string&mdnsName,bool addTestGroup = false); + int portNumber, const std::string &name, const std::string &instanceId, const std::string&mdnsName, bool wait); - void Unannounce(); + void Unannounce(bool wait); private: + enum class ServiceState { + Unitialized, + Initializing, + Settling, + Requested, + Uncommited, + Registering, + Collision, + Reset, + Established, + Failed, + Closed + }; + + std::atomic serviceState; + void Wait(); + void WaitForUnannounce(); + void SetState(ServiceState serviceState); + + std::atomic terminated = false; + bool stopped = false; + bool started = false; + bool createPending = false; + bool makeAnnouncement = false; + void Start(); void Stop(); static void entry_group_callback(AvahiEntryGroup*g, AvahiEntryGroupState state, void *userData); @@ -59,13 +86,13 @@ namespace pipedal { int portNumber = -1; std::string instanceId; + std::string serviceName; std::string mdnsName; - bool addTestGroup = false; AvahiClient *client = NULL; AvahiEntryGroup *group = NULL; AvahiThreadedPoll *threadedPoll = NULL; - char*name = NULL; + char*avahiNameString = NULL; }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7841428..e0f61b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -96,7 +96,7 @@ endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) -set (USE_SANITIZE False) +set (USE_SANITIZE true) if (!ENABLE_VST3) diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 423298c..f3d860a 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -1295,6 +1295,7 @@ void RequireNetworkManager() throw std::runtime_error("The current OS is not using NetworkManager."); } } + int main(int argc, char **argv) { CommandLineParser parser; diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp index c680422..bb7b6a9 100644 --- a/src/HotspotManager.cpp +++ b/src/HotspotManager.cpp @@ -54,13 +54,12 @@ namespace pipedal::impl std::vector GetKnownWifiNetworks(); - virtual PostHandle Post(PostCallback&&fn) override; - virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn) override; + virtual PostHandle Post(PostCallback &&fn) override; + virtual PostHandle PostDelayed(const clock::duration &delay, PostCallback &&fn) override; virtual bool CancelPost(PostHandle handle) override; virtual void SetNetworkChangingListener(NetworkChangingListener &&listener) override; - private: enum class State { @@ -104,8 +103,7 @@ namespace pipedal::impl void UpdateKnownNetworks( std::vector &knownSsids, - std::vector &allAccessPoints - ); + std::vector &allAccessPoints); void FireNetworkChanging(); @@ -151,7 +149,6 @@ using namespace pipedal::impl; HotspotManagerImpl::HotspotManagerImpl() { - SetDBusLogLevel(DBusLogLevel::None); } void HotspotManagerImpl::Open() @@ -167,7 +164,24 @@ void HotspotManagerImpl::Open() void HotspotManagerImpl::onClose() { - StopHotspot(); + this->closed = true; // avoids a memory barrier probelm. + + CancelDeviceChangedTimer(); + CancelWaitForNetworkManagerTimer(); + CancelAccessPointsChangedTimer(); + + + if (networkManager && activeConnection) + { + try { + networkManager->DeactivateConnection(activeConnection->getObjectPath()); + activeConnection = nullptr; + } catch (const std::exception&e) + { + // nothrow. + } + } + ReleaseNetworkManager(); Lv2Log::debug("HotspotManager: state=Closed"); @@ -250,6 +264,7 @@ void HotspotManagerImpl::UpdateNetworkManagerStatus() this->StartWaitForNetworkManagerTimer(); } } + void HotspotManagerImpl::CancelDeviceChangedTimer() { if (devicesChangedTimerHandle) @@ -386,7 +401,6 @@ void HotspotManagerImpl::onStartMonitoring() StartScanTimer(); onAccessPointChanged(); - } catch (const std::exception &e) { @@ -397,6 +411,7 @@ void HotspotManagerImpl::onStartMonitoring() void HotspotManagerImpl::StartWaitForNetworkManagerTimer() { + CancelWaitForNetworkManagerTimer(); networkManagerTimerHandle = dbusDispatcher.PostDelayed( std::chrono::seconds(5), @@ -408,6 +423,7 @@ void HotspotManagerImpl::StartWaitForNetworkManagerTimer() } void HotspotManagerImpl::CancelWaitForNetworkManagerTimer() { + if (networkManagerTimerHandle) { dbusDispatcher.CancelPost(networkManagerTimerHandle); @@ -436,7 +452,7 @@ void HotspotManagerImpl::onReload() case State::Error: // ignore. return; - default: + default: MaybeStartHotspot(); return; } @@ -473,8 +489,6 @@ void HotspotManagerImpl::Close() } } - - HotspotManager::ptr HotspotManager::Create() { return std::make_unique(); @@ -496,8 +510,8 @@ public: } }; - -struct NetworkSortRecord { +struct NetworkSortRecord +{ std::string ssid; bool connected = false; bool knownNetwork = false; @@ -506,12 +520,11 @@ struct NetworkSortRecord { }; void HotspotManagerImpl::UpdateKnownNetworks( std::vector &knownSsids, - std::vector & allAccessPoints -) + std::vector &allAccessPoints) { - std::map map; + std::map map; - for (auto &knownSsid: knownSsids) + for (auto &knownSsid : knownSsids) { std::string ssid = ssidToString(knownSsid); if (ssid.length() != 0) @@ -521,17 +534,22 @@ void HotspotManagerImpl::UpdateKnownNetworks( record.knownNetwork = true; } } - for (auto&accessPoint: allAccessPoints) + for (auto &accessPoint : allAccessPoints) { - uint8_t strength = accessPoint->Strength(); - auto vSsid = accessPoint->Ssid(); - std::string ssid = ssidToString(vSsid); - if (ssid.length() != 0) + try { + uint8_t strength = accessPoint->Strength(); + auto vSsid = accessPoint->Ssid(); + std::string ssid = ssidToString(vSsid); + if (ssid.length() != 0) + { + NetworkSortRecord &record = map[ssid]; + record.ssid = ssid; + record.visibleNetwork = true; + record.strength = strength; + } + } catch (const std::exception&ignored) { - NetworkSortRecord&record = map[ssid]; - record.ssid = ssid; - record.visibleNetwork = true; - record.strength = strength; + // race to get the info before it changes. np. } } if (this->wlanDevice) @@ -539,24 +557,23 @@ void HotspotManagerImpl::UpdateKnownNetworks( auto activeConnectionPath = this->wlanDevice->ActiveConnection(); if (activeConnectionPath.length() > 2) // "/" -> no connection. Be paranoid about "". { - auto activeConnection = ActiveConnection::Create(dbusDispatcher,activeConnectionPath); + auto activeConnection = ActiveConnection::Create(dbusDispatcher, activeConnectionPath); std::string activeSsid = activeConnection->Id(); auto it = map.find(activeSsid); if (it != map.end()) { it->second.connected = true; } - } } std::vector records; records.reserve(map.size()); - for (auto & mapEntry: map) + for (auto &mapEntry : map) { records.push_back(mapEntry.second); } - std::sort(records.begin(),records.end(), [](const NetworkSortRecord&left, const NetworkSortRecord &right) - { + std::sort(records.begin(), records.end(), [](const NetworkSortRecord &left, const NetworkSortRecord &right) + { if (left.connected != right.connected) { return left.connected > right.connected; @@ -573,27 +590,25 @@ void HotspotManagerImpl::UpdateKnownNetworks( { return left.strength > right.strength; } - return false; - }); + return false; }); if (records.size() > 10) { records.resize(10); } std::vector result; result.reserve(records.size()); - for (auto &record: records) + for (auto &record : records) { result.push_back(std::move(record.ssid)); } { - std::lock_guard lock { this->knownWifiNetworksMutex }; + std::lock_guard lock{this->knownWifiNetworksMutex}; this->knownWifiNetworks = std::move(result); } - } - -std::vector HotspotManagerImpl::GetAllAccessPoints() { +std::vector HotspotManagerImpl::GetAllAccessPoints() +{ std::vector accessPoints; for (const auto &accessPointPath : wlanWirelessDevice->AccessPoints()) @@ -602,15 +617,14 @@ std::vector HotspotManagerImpl::GetAllAccessPoints() { accessPoints.push_back(std::move(accessPoint)); } return accessPoints; - } -std::vector> HotspotManagerImpl::GetKnownVisibleAccessPoints(const std::vector&allAccessPoints) +std::vector> HotspotManagerImpl::GetKnownVisibleAccessPoints(const std::vector &allAccessPoints) { std::vector> knownSsids = this->GetAllAutoConnectSsids(); std::unordered_set, VectorHash> index{knownSsids.begin(), knownSsids.end()}; - std::vector result; + std::vector result; std::vector accessPoints; for (const auto &accessPoint : allAccessPoints) @@ -684,14 +698,16 @@ void HotspotManagerImpl::onAccessPointChanged() } } -static std::vector> GetAccessPointSsids(std::vector&accessPoints) +static std::vector> GetAccessPointSsids(std::vector &accessPoints) { std::vector> result; - for (const auto&accessPoint: accessPoints) + for (const auto &accessPoint : accessPoints) { - try { + try + { result.push_back(accessPoint->Ssid()); - } catch (const std::exception&ignored) + } + catch (const std::exception &ignored) { // race to get a disappearing ssid. Ignore the error. } @@ -700,8 +716,10 @@ static std::vector> GetAccessPointSsids(std::vectorstate == State::Error) return; - if (this->closed) return; + if (this->state == State::Error) + return; + if (this->closed) + return; if (!wlanDevice || !wlanWirelessDevice) { @@ -711,12 +729,12 @@ void HotspotManagerImpl::MaybeStartHotspot() std::vector allAccessPoints = GetAllAccessPoints(); std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. - - this->UpdateKnownNetworks(connectableSsids,allAccessPoints); - bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + this->UpdateKnownNetworks(connectableSsids, allAccessPoints); - this->ethernetConnected,connectableSsids,allAccessPointSsids); + bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + + this->ethernetConnected, connectableSsids, allAccessPointSsids); if (this->state == State::Monitoring && wantsHotspot) { @@ -764,7 +782,7 @@ Connection::ptr HotspotManagerImpl::FindExistingConnection() } } } - + return nullptr; } @@ -874,28 +892,26 @@ void HotspotManagerImpl::StopHotspot() { if (activeConnection) { + FireNetworkChanging(); networkManager->DeactivateConnection(activeConnection->getObjectPath()); activeConnection = nullptr; - - FireNetworkChanging(); - } } catch (const std::exception &e) { - Lv2Log::error("HotspotManager: Failed to deactivate hotspot."); + Lv2Log::error(SS("HotspotManager: Failed to deactivate hotspot. " << e.what())); activeConnection = nullptr; } if (this->state == State::HotspotConnected || this->state == State::HotspotConnecting) { - Lv2Log::info("HotspotManager: state=HotspotMonitoring"); + Lv2Log::debug("HotspotManager: state=HotspotMonitoring"); SetState(State::Monitoring); Lv2Log::info("HotspotManager: PiPedal hotspot disabled."); } } -static const std::chrono::seconds scanInterval { 60}; +static const std::chrono::seconds scanInterval{60}; void HotspotManagerImpl::StartScanTimer() { @@ -904,7 +920,8 @@ void HotspotManagerImpl::StartScanTimer() } void HotspotManagerImpl::StopScanTimer() { - if (this->scanTimerHandle) { + if (this->scanTimerHandle) + { dbusDispatcher.CancelPost(this->scanTimerHandle); this->scanTimerHandle = 0; } @@ -913,14 +930,17 @@ void HotspotManagerImpl::ScanNow() { this->scanTimerHandle = 0; - if (wlanWirelessDevice) { + if (wlanWirelessDevice) + { std::map options; - try { + try + { Lv2Log::debug("Scanning"); wlanWirelessDevice->RequestScan(options); - } catch (const std::exception &e) + } + catch (const std::exception &e) { Lv2Log::error(SS("HotspotMonitor: Wi-Fi RequestScan failed." << e.what())); return; @@ -928,44 +948,42 @@ void HotspotManagerImpl::ScanNow() this->scanTimerHandle = this->dbusDispatcher.PostDelayed( std::chrono::duration_cast(scanInterval), - [this]() { + [this]() + { ScanNow(); - } - ); + }); } } -HotspotManagerImpl::PostHandle HotspotManagerImpl::Post(PostCallback&&fn) +HotspotManagerImpl::PostHandle HotspotManagerImpl::Post(PostCallback &&fn) { return dbusDispatcher.Post(std::move(fn)); } -HotspotManagerImpl::PostHandle HotspotManagerImpl::PostDelayed(const clock::duration&delay,PostCallback&&fn) +HotspotManagerImpl::PostHandle HotspotManagerImpl::PostDelayed(const clock::duration &delay, PostCallback &&fn) { - return dbusDispatcher.PostDelayed(delay,std::move(fn)); - + return dbusDispatcher.PostDelayed(delay, std::move(fn)); } -bool HotspotManagerImpl::CancelPost(PostHandle handle) { +bool HotspotManagerImpl::CancelPost(PostHandle handle) +{ return dbusDispatcher.CancelPost(handle); } -void HotspotManagerImpl::SetNetworkChangingListener(NetworkChangingListener &&listener) +void HotspotManagerImpl::SetNetworkChangingListener(NetworkChangingListener &&listener) { - std::lock_guard lock { this->networkChangingListenerMutex}; + std::lock_guard lock{this->networkChangingListenerMutex}; this->networkChangingListener = std::move(listener); } -void HotspotManagerImpl::FireNetworkChanging() { - std::lock_guard lock { this->networkChangingListenerMutex}; +void HotspotManagerImpl::FireNetworkChanging() +{ + std::lock_guard lock{this->networkChangingListenerMutex}; if (this->networkChangingListener) { - this->networkChangingListener(this->ethernetConnected,!!activeConnection); + this->networkChangingListener(this->ethernetConnected, !!activeConnection); } - } -std::vector HotspotManagerImpl::GetKnownWifiNetworks() +std::vector HotspotManagerImpl::GetKnownWifiNetworks() { std::lock_guard lock(knownWifiNetworksMutex); // pushed off the service thread. return this->knownWifiNetworks; - } - diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 358d7b7..8d256f2 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -39,6 +39,9 @@ #include "DBusToLv2Log.hpp" #include "SysExec.hpp" #include "Updater.hpp" +#include "util.hpp" +#include "DBusLog.hpp" +#include "AvahiService.hpp" #ifndef NO_MLOCK #include @@ -88,6 +91,7 @@ PiPedalModel::PiPedalModel() DbusLogToLv2Log(); + SetDBusLogLevel(DBusLogLevel::Info); hotspotManager = HotspotManager::Create(); hotspotManager->SetNetworkChangingListener( @@ -100,24 +104,41 @@ PiPedalModel::PiPedalModel() void PiPedalModel::Close() { - std::lock_guard lock(mutex); + std::unique_ptr oldAudioHost; + { + std::lock_guard lock(mutex); + if (closed) + { + return; + } + closed = true; - // 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()]; - for (size_t i = 0; i < subscribers.size(); ++i) - { - t[i] = this->subscribers[i]; - } - size_t n = this->subscribers.size(); - for (size_t i = 0; i < n; ++i) - { - t[i]->Close(); - } - delete[] t; + if (avahiService) { + this->avahiService = nullptr; // and close. + } - if (audioHost) + // 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()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + for (size_t i = 0; i < n; ++i) + { + t[i]->Close(); + } + delete[] t; + + this->subscribers.resize(0); + + oldAudioHost = std::move(this->audioHost); + } // end lock. + + // lockless to avoid deadlocks while shutting down the audio thread. + if (oldAudioHost) { - audioHost->Close(); + oldAudioHost->Close(); } } @@ -977,11 +998,11 @@ void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSet #if NEW_WIFI_CONFIG if (this->storage.SetWifiConfigSettings(wifiConfigSettings)) { + this->UpdateDnsSd(); if (this->hotspotManager) { this->hotspotManager->Reload(); } - this->UpdateDnsSd(); } #else this->storage.SetWifiConfigSettings(wifiConfigSettings); @@ -1022,20 +1043,28 @@ static std::string GetP2pdName() void PiPedalModel::UpdateDnsSd() { - // avahiService.Unannounce(); let Announce decide whether it wants to unannounce or update. - + if (!avahiService) + { + throw std::runtime_error("Not ready."); + } ServiceConfiguration deviceIdFile; deviceIdFile.Load(); - - std::string p2pdName = GetP2pdName(); - if (p2pdName != "") + WifiConfigSettings wifiSettings; + wifiSettings.Load(); + std::string serviceName = wifiSettings.hotspotName_; + if (serviceName == "") { - deviceIdFile.deviceName = p2pdName; + serviceName = deviceIdFile.deviceName; + } + if (serviceName == "") + { + serviceName = "pipedal"; } - if (deviceIdFile.deviceName != "" && deviceIdFile.uuid != "") + std::string hostName = GetHostName(); + if (serviceName != "" && deviceIdFile.uuid != "") { - avahiService.Announce(webPort, deviceIdFile.deviceName, deviceIdFile.uuid, "pipedal"); + avahiService->Announce(webPort, serviceName, deviceIdFile.uuid, hostName,true); } else { @@ -1376,12 +1405,19 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() addedInstances.insert(activeVuSubscriptions[i].instanceid); } } - std::vector instanceids(addedInstances.begin(), addedInstances.end()); - audioHost->SetVuSubscriptions(instanceids); + if (audioHost) + { + std::vector instanceids(addedInstances.begin(), addedInstances.end()); + audioHost->SetVuSubscriptions(instanceids); + } } void PiPedalModel::UpdateRealtimeMonitorPortSubscriptions() { + if (!audioHost) + { + return; + } audioHost->SetMonitorPortSubscriptions(this->activeMonitorPortSubscriptions); } @@ -1484,7 +1520,10 @@ void PiPedalModel::SendSetPatchProperty( clientId, instanceId, urid, atomValue, nullptr, onError); outstandingParameterRequests.push_back(request); - this->audioHost->sendRealtimeParameterRequest(request); + if (this->audioHost) + { + this->audioHost->sendRealtimeParameterRequest(request); + } } void PiPedalModel::SendGetPatchProperty( @@ -1545,7 +1584,10 @@ void PiPedalModel::SendGetPatchProperty( std::lock_guard lock(mutex); outstandingParameterRequests.push_back(request); - this->audioHost->sendRealtimeParameterRequest(request); + if (this->audioHost) + { + this->audioHost->sendRealtimeParameterRequest(request); + } } BankIndex PiPedalModel::GetBankIndex() const @@ -1802,7 +1844,10 @@ void PiPedalModel::DeleteAtomOutputListeners(int64_t clientId) --i; } } - audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + if (audioHost) + { + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + } } void PiPedalModel::DeleteMidiListeners(int64_t clientId) @@ -1816,7 +1861,10 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) --i; } } - audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + if (audioHost) + { + audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + } } void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) @@ -2248,6 +2296,12 @@ static bool HasAlsaDevice(const std::vector devices, const std:: void PiPedalModel::StartHotspotMonitoring() { + this->avahiService = std::make_unique(); + + SetThreadName("avahi"); // hack to name the avahi service thread. + UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. + SetThreadName("main"); + this->hotspotManager->Open(); } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 1487fd7..042930b 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -36,7 +36,6 @@ #include "WifiConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp" #include "AdminClient.hpp" -#include "AvahiService.hpp" #include #include "Promise.hpp" #include "AtomConverter.hpp" @@ -49,6 +48,7 @@ namespace pipedal struct RealtimeNextMidiProgramRequest; class Lv2PluginChangeMonitor; class Updater; + class AvahiService; class IPiPedalModelSubscriber { @@ -113,7 +113,7 @@ namespace pipedal std::vector systemMidiBindings; - AvahiService avahiService; + std::unique_ptr avahiService; uint16_t webPort; PiPedalAlsaDevices alsaDevices; @@ -197,6 +197,7 @@ namespace pipedal std::vector outstandingParameterRequests; IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId); + std::atomic closed = false; private: // IAudioHostCallbacks virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override; diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 558602a..23a9f78 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -516,26 +516,47 @@ private: std::mutex activePortMonitorsMutex; std::vector> activePortMonitors; + std::atomic closed = false; public: virtual int64_t GetClientId() { return clientId; } virtual ~PiPedalSocketHandler() { + if (!closed) + { + FinalCleanup(); + } + } + + bool finalCleanup = false; + void FinalCleanup() + { + if (finalCleanup) return; + finalCleanup = true; + // avoid use after free. for (int i = 0; i < this->activePortMonitors.size(); ++i) { model.UnmonitorPort(activePortMonitors[i]->subscriptionHandle); } + activePortMonitors.resize(0); for (int i = 0; i < this->activeVuSubscriptions.size(); ++i) { model.RemoveVuSubscription(activeVuSubscriptions[i].subscriptionHandle); } + activeVuSubscriptions.resize(0); model.RemoveNotificationSubsription(this); + } virtual void Close() { + if (closed) return; + closed = true; + + FinalCleanup(); // do it while we can. &model will no longer be valid after this. ( :-( ) + SocketHandler::Close(); } @@ -935,6 +956,10 @@ public: } return; } + if (closed) + { + this->SendError(replyTo, "Server has shut down."); + } if (message == "setControl") { ControlChangedBody message; diff --git a/src/Updater.cpp b/src/Updater.cpp index d4a9037..f13f15a 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -1181,7 +1181,7 @@ void Updater::ValidateSignature(const std::filesystem::path &file, const std::fi << " --verify " << signatureFile << " " << file; - Lv2Log::info(SS("/usr/bin/gpg " << ss.str())); + Lv2Log::debug(SS("/usr/bin/gpg " << ss.str())); auto gpgOutput = sysExecForOutput("/usr/bin/gpg", ss.str()); if (gpgOutput.exitCode != EXIT_SUCCESS) { @@ -1191,17 +1191,20 @@ void Updater::ValidateSignature(const std::filesystem::path &file, const std::fi if (!IsSignatureGood(gpgText)) { + Lv2Log::error(gpgOutput.output); throw std::runtime_error("Update signature is not valid."); } std::string keyId = getFingerprint(gpgText); if (keyId != UPDATE_GPG_FINGERPRINT && keyId != UPDATE_GPG_FINGERPRINT2) { + Lv2Log::error(gpgOutput.output); throw std::runtime_error(SS("Update signature has the wrong id: " << keyId)); } std::string origin = getAddress(gpgText); if (origin != UPDATE_GPG_ADDRESS && origin != UPDATE_GPG_ADDRESS2) { + Lv2Log::error(gpgOutput.output); throw std::runtime_error(SS("Update signature has an incorrect address." << origin)); } } diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 7ffc570..5108545 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -557,16 +557,6 @@ pipedal::last_modified(const std::filesystem::path &path) } } -static std::string getHostName() -{ - char buff[512]; - if (gethostname(buff, sizeof(buff)) == 0) - { - buff[511] = '\0'; - return buff; - } - return ""; -} static std::string getIpv4Address(const std::string interface) { @@ -1165,7 +1155,7 @@ namespace pipedal std::stringstream ss; ss << port; - // m_endpoint.listen(this->address, ss.str()); + //m_endpoint.listen(this->address, ss.str()); m_endpoint.listen(tcp::v6(), (uint16_t)port); m_endpoint.start_accept(); @@ -1305,7 +1295,7 @@ std::shared_ptr pipedal::WebServer::create( void WebServerImpl::DisplayIpAddresses() { - std::string hostName = getHostName(); + std::string hostName = GetHostName(); if (hostName.length() != 0) { std::stringstream ss; diff --git a/src/WebServerLog.hpp b/src/WebServerLog.hpp index 26a3d65..56ad8f8 100644 --- a/src/WebServerLog.hpp +++ b/src/WebServerLog.hpp @@ -117,7 +117,7 @@ public: /// Write a string message to the given channel /** - * @param channel The channel to write to + * @param channel The channel to write tosu * @param msg The message to write */ void write(level channel, std::string const & msg) { @@ -126,19 +126,19 @@ public: { case elevel::devel: case elevel::library: - Lv2Log::debug(msg); + Lv2Log::debug("WebServer: %s",msg.c_str()); break; case elevel::info: - Lv2Log::info(msg); + Lv2Log::info("WebServer: %s",msg.c_str()); break; case elevel::warn: - Lv2Log::warning(msg); + Lv2Log::warning("WebServer: %s",msg.c_str()); break; case elevel::rerror: - Lv2Log::error(msg); + Lv2Log::error("WebServer: %s",msg.c_str()); break; case elevel::fatal: - Lv2Log::error("Fatal error: " + msg); + Lv2Log::error("WebServer fatal error: %s",msg.c_str()); break; default: break; @@ -159,7 +159,7 @@ public: } bool dynamic_test(level channel) { - return (m_channels) & channel != 0; + return (m_channels & channel) != 0; } protected: diff --git a/src/main.cpp b/src/main.cpp index 1e9cb82..0987066 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -81,10 +81,18 @@ static bool isJackServiceRunning() return std::filesystem::exists(path); } + +static void AsanCheck() +{ + char *t = new char[5]; + t[5] = 'x'; + delete t; + exit(EXIT_FAILURE); +} + int main(int argc, char *argv[]) { - #ifndef WIN32 umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction. #endif @@ -269,7 +277,6 @@ int main(int argc, char *argv[]) (unsigned long)getpid()); } - model.StartHotspotMonitoring(); model.WaitForAudioDeviceToComeOnline(); @@ -317,9 +324,9 @@ int main(int argc, char *argv[]) { server->RunInBackground(-1); - SetThreadName("avahi"); - model.UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. - SetThreadName("main"); + + model.StartHotspotMonitoring(); + { sigwait(&sigSet, &sig);