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/AppThemed.tsx b/react/src/AppThemed.tsx index 19965bc..56829fc 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -59,19 +59,19 @@ import { BankIndex, BankIndexEntry } from './Banks'; import RenameDialog from './RenameDialog'; import JackStatusView from './JackStatusView'; import { Theme } from '@mui/material/styles'; -import {isDarkMode} from './DarkMode'; +import { isDarkMode } from './DarkMode'; -import {ReactComponent as RenameOutlineIcon} from './svg/drive_file_rename_outline_black_24dp.svg'; -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 { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg'; +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'; const appStyles = (theme: Theme) => createStyles({ "&": { // :root - colorScheme: (isDarkMode()? "dark": "light") + colorScheme: (isDarkMode() ? "dark" : "light") }, menuListItem: { color: "#FE8!important" as any, //theme.palette.text.primary, @@ -95,7 +95,7 @@ const appStyles = (theme: Theme) => createStyles({ top: "0px", width: "100%", height: "100%", - background: isDarkMode() ? "#222": "#DDD", + background: isDarkMode() ? "#222" : "#DDD", opacity: "0.95", justifyContent: "center", textAlign: "center", @@ -114,13 +114,13 @@ const appStyles = (theme: Theme) => createStyles({ top: "0px", width: "100%", height: "100%", - color: isDarkMode() ? "#CCC": "#444", + color: isDarkMode() ? "#CCC" : "#444", zIndex: 2000 }, errorContentMask: { position: "absolute", left: 0, top: 0, width: "100%", height: "100%", - background: isDarkMode()? "#121212": "#BBB", + background: isDarkMode() ? "#121212" : "#BBB", opacity: 0.95, zIndex: 1999 }, @@ -511,7 +511,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< } private unmountListener(e: Event) { - if ((!this.model_.reloadRequested) && this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) { + if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) { e.preventDefault(); (e as any).returnValue = "Are you sure you want to leave this page?"; return "Are you sure you want to leave this page?"; @@ -521,7 +521,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< componentDidMount() { super.componentDidMount(); - window.addEventListener("beforeunload",this.unmountListener); + window.addEventListener("beforeunload", this.unmountListener); this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_); this.model_.state.addOnChangedHandler(this.stateChangeHandler_); @@ -533,417 +533,429 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< } -updateOverscroll(): void { - if(this.model_.serverVersion) { - // no pull-down refresh on android devices once we're ready (unless we're debug) - let preventOverscroll = - this.model_.state.get() === State.Ready - && !this.model_.debug; + updateOverscroll(): void { + if (this.model_.serverVersion) { + // no pull-down refresh on android devices once we're ready (unless we're debug) + let preventOverscroll = + this.model_.state.get() === State.Ready + && !this.model_.debug; - let overscrollBehavior = preventOverscroll ? "none" : "auto"; - document.body.style.overscrollBehavior = overscrollBehavior; -} + let overscrollBehavior = preventOverscroll ? "none" : "auto"; + document.body.style.overscrollBehavior = overscrollBehavior; + } } -componentDidUpdate() { -} + componentDidUpdate() { + } -componentWillUnmount() { - super.componentWillUnmount(); - window.removeEventListener("beforeunload",this.unmountListener); + componentWillUnmount() { + super.componentWillUnmount(); + window.removeEventListener("beforeunload", this.unmountListener); - this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_); - this.model_.state.removeOnChangedHandler(this.stateChangeHandler_); - this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler); - this.model_.banks.removeOnChangedHandler(this.banksChangedHandler); - this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler); + this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_); + this.model_.state.removeOnChangedHandler(this.stateChangeHandler_); + this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler); + this.model_.banks.removeOnChangedHandler(this.banksChangedHandler); + this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler); -} + } + + alertMessageChangedHandler() { + let message = this.model_.alertMessage.get(); + if (message === "") { + this.setState({ alertDialogOpen: false }); + // leave the message intact so the dialog can fade. + } else { + this.setState({ + alertDialogOpen: true, + alertDialogMessage: message + }); + } + + } + updateResponsive() { + // functional, but disabled. + // let tinyToolBar_ = this.windowSize.height < 600; + // this.setState({ tinyToolBar: tinyToolBar_ }); + + let height = this.windowSize.height; + + const ENTRY_HEIGHT = 48; + // ENTRY_HEIGHT*6 +K = 727 from observation. + const K = 450; + + let bankEntries = Math.floor((height - K) / ENTRY_HEIGHT); + if (bankEntries < 1) bankEntries = 1; + if (bankEntries > 7) bankEntries = 7; + this.setState({ bankDisplayItems: bankEntries }); + + } + onWindowSizeChanged(width: number, height: number): void { + super.onWindowSizeChanged(width, height); + this.updateResponsive(); + } + + + setErrorMessage(message: string): void { + this.setState({ errorMessage: message }); + } + + + setDisplayState(newState: State): void { + this.updateOverscroll(); -alertMessageChangedHandler() { - let message = this.model_.alertMessage.get(); - if (message === "") { - this.setState({ alertDialogOpen: false }); - // leave the message intact so the dialog can fade. - } else { this.setState({ - alertDialogOpen: true, - alertDialogMessage: message + displayState: newState, + canFullScreen: supportsFullScreen() && !this.model_.isAndroidHosted() }); - } - -} -updateResponsive() { - // functional, but disabled. - // let tinyToolBar_ = this.windowSize.height < 600; - // this.setState({ tinyToolBar: tinyToolBar_ }); - - let height = this.windowSize.height; - - const ENTRY_HEIGHT = 48; - // ENTRY_HEIGHT*6 +K = 727 from observation. - const K = 450; - - let bankEntries = Math.floor((height - K) / ENTRY_HEIGHT); - if (bankEntries < 1) bankEntries = 1; - if (bankEntries > 7) bankEntries = 7; - this.setState({ bankDisplayItems: bankEntries }); - -} -onWindowSizeChanged(width: number, height: number): void { - super.onWindowSizeChanged(width, height); - this.updateResponsive(); -} - - -setErrorMessage(message: string): void { - this.setState({ errorMessage: message }); -} - - -setDisplayState(newState: State): void { - this.updateOverscroll(); - - this.setState({ - displayState: newState, - canFullScreen: supportsFullScreen() && !this.model_.isAndroidHosted() - }); - if(newState === State.Ready) -{ - if (this.model_.isOnboarding()) { - this.handleDisplayOnboarding(); - } -} - } - -showDrawer() { - this.setState({ isDrawerOpen: true }) -} -hideDrawer() { - this.setState({ isDrawerOpen: false }) -} -shortBankList(banks: BankIndex): BankIndexEntry[] { - let n = this.state.bankDisplayItems; - let entries = banks.entries; - if (entries.length < n + 1) { // +1 for the .... entry. - return entries; - } - let result: BankIndexEntry[] = []; - let selectedIndex = -1; - for (let i = 0; i < entries.length; ++i) { - if (entries[i].instanceId === banks.selectedBank) { - selectedIndex = i; - break; + if (newState === State.Ready) { + if (this.model_.isOnboarding()) { + this.handleDisplayOnboarding(); + } } } - if (n > entries.length) n = entries.length; - if (selectedIndex > n) { - for (let i = 0; i < n - 1; ++i) { - result.push(entries[i]); + + showDrawer() { + this.setState({ isDrawerOpen: true }) + } + hideDrawer() { + this.setState({ isDrawerOpen: false }) + } + shortBankList(banks: BankIndex): BankIndexEntry[] { + let n = this.state.bankDisplayItems; + let entries = banks.entries; + if (entries.length < n + 1) { // +1 for the .... entry. + return entries; } - result.push(entries[selectedIndex]); - } else { - for (let i = 0; i < n; ++i) { - result.push(entries[i]); + let result: BankIndexEntry[] = []; + let selectedIndex = -1; + for (let i = 0; i < entries.length; ++i) { + if (entries[i].instanceId === banks.selectedBank) { + selectedIndex = i; + break; + } + } + if (n > entries.length) n = entries.length; + if (selectedIndex > n) { + for (let i = 0; i < n - 1; ++i) { + result.push(entries[i]); + } + result.push(entries[selectedIndex]); + } else { + for (let i = 0; i < n; ++i) { + result.push(entries[i]); + } + } + return result; + } + handleReload() { + if (this.model_.isAndroidHosted()) { + this.model_.chooseNewDevice(); + } else { + window.location.reload(); } } - return result; -} -handleReload() { - if (this.model_.isAndroidHosted()) { - this.model_.chooseNewDevice(); - } else { - window.location.reload(); + getReloadingMessage(): string { + switch (this.state.displayState) { + case State.ApplyingChanges: + return "Applying\u00A0changes..."; + case State.ReloadingPlugins: + return "Reloading\u00A0plugins..."; + default: + return "Reconnecting..."; + } } -} -render() { + render() { - const { classes } = this.props; + const { classes } = this.props; - let shortBankList = this.shortBankList(this.state.banks); - let showBankSelectDialog = shortBankList.length !== this.state.banks.entries.length; + let shortBankList = this.shortBankList(this.state.banks); + let showBankSelectDialog = shortBankList.length !== this.state.banks.entries.length; - return ( -
{ - if (!this.model_.debug) { - e.preventDefault(); e.stopPropagation(); - } + return ( +
- - {(!this.state.tinyToolBar) ? - ( - - - { this.showDrawer() }} - size="large"> - - -
- -
-
- {this.state.canFullScreen && + onContextMenu={(e) => { + if (!this.model_.debug) { + e.preventDefault(); e.stopPropagation(); + } + }} + > + + {(!this.state.tinyToolBar) ? + ( + + { this.toggleFullScreen(); }} color="inherit" + onClick={() => { this.showDrawer() }} + size="large"> + + +
+ +
+
+ {this.state.canFullScreen && + { this.toggleFullScreen(); }} + color="inherit" + size="large"> + {this.state.isFullScreen ? ( + + ) : ( + + + )} + + + } + + + ) : ( +
+ { this.showDrawer() }} + color="inherit" + size="large"> + + + {this.state.canFullScreen && ( + { this.toggleFullScreen(); }} size="large"> {this.state.isFullScreen ? ( - + ) : ( - + )} - } - - - ) : ( -
- { this.showDrawer() }} - color="inherit" - size="large"> - - - {this.state.canFullScreen && ( - { this.toggleFullScreen(); }} - size="large"> - {this.state.isFullScreen ? ( - - ) : ( - + )} +
+ )} + { this.hideDrawer(); }} > + + Banks - )} + + { + shortBankList.map((bank) => { + return ( + this.onOpenBank(bank.instanceId)} + > - - )} -
- )} - { this.hideDrawer(); }} > - - Banks + + - - { - shortBankList.map((bank) => { - return ( - this.onOpenBank(bank.instanceId)} + ); + }) + } + { + showBankSelectDialog && ( + this.handleDrawerSelectBank()} > - + - ); + + ) + } + + + + { this.handleDrawerRenameBank() }}> + + + + + + { this.handleDrawerSaveBankAs() }} > + + + + + + { this.handleDrawerManageBanks(); }}> + + + + + + + + + { this.handleDrawerSettingsClick() }}> + + + + + + { this.handleDrawerAboutClick() }}> + + + + + + { this.handleDrawerDonationClick() }}> + + + + + + + + + {!this.state.tinyToolBar && ( + + )} +
+
+
+ {(this.state.displayState !== State.Loading) && ( + + )} +
+ +
+
+ this.setState({ bankDialogOpen: false })} /> + this.setState({ aboutDialogOpen: false })} /> + this.handleSettingsDialogClose()} /> + { + this.setState({ + renameBankDialogOpen: false, + saveBankAsDialogOpen: false }) + }} + onOk={(text: string) => { + if (this.state.renameBankDialogOpen) { + this.handleBankRenameOk(text); + } else if (this.state.saveBankAsDialogOpen) { + this.handleSaveBankAsOk(text); + } } - { - showBankSelectDialog && ( - this.handleDrawerSelectBank()} - > - - - - - - ) } - - - - { this.handleDrawerRenameBank() }}> - - - - - - { this.handleDrawerSaveBankAs() }} > - - - - - - { this.handleDrawerManageBanks(); }}> - - - - - - - - - { this.handleDrawerSettingsClick() }}> - - - - - - { this.handleDrawerAboutClick() }}> - - - - - - { this.handleDrawerDonationClick() }}> - - - - - - - - - {!this.state.tinyToolBar && ( - - )} -
-
-
- {(this.state.displayState !== State.Loading) && ( - - )} -
-
-
- this.setState({ bankDialogOpen: false })} /> - this.setState({ aboutDialogOpen: false })} /> - this.handleSettingsDialogClose()} /> - { - this.setState({ - renameBankDialogOpen: false, - saveBankAsDialogOpen: false - }) - }} - onOk={(text: string) => { - if (this.state.renameBankDialogOpen) { - this.handleBankRenameOk(text); - } else if (this.state.saveBankAsDialogOpen) { - this.handleSaveBankAsOk(text); + { this.setState({ zoomedControlOpen: false }); }} + onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); } } - } - } - /> - - { this.setState({ zoomedControlOpen: false }); }} - onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); } - } - /> - - - - - { - this.state.alertDialogMessage - } - - - - - - - - {this.state.showStatusMonitor && ()} - -
-
- -
-
- -
- - {this.state.displayState === State.ApplyingChanges ? "Applying\u00A0changes..." : "Reconnecting..."} - -
-
-
-
-
 
-
-
- -
-
-

- Error: {this.state.errorMessage} -

-
-
- + + + {this.state.showStatusMonitor && ()} + +
+
+ +
+
+ +
+ + {this.getReloadingMessage()} + +
+
+
+
+
 
+
+
+ +
+
+

+ Error: {this.state.errorMessage} +

+
+
+ + +
+
 
+
- -
 
- -
-
-
-
-
- +
+
+
+
+ +
+ + Loading... +
- - Loading... -
-
-
- ); -} +
+ ); + } } ); diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index 7baaef3..860ab48 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -42,6 +42,7 @@ import SearchFilter from './SearchFilter'; import { FixedSizeGrid } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import StarBorderIcon from '@mui/icons-material/StarBorder'; +import StarIcon from '@mui/icons-material/Star'; import { createStyles, Theme } from '@mui/material/styles'; import { WithStyles, withStyles } from '@mui/styles'; import FilterListIcon from '@mui/icons-material/FilterList'; @@ -172,8 +173,9 @@ type PluginGridState = { grid_cell_width: number, grid_cell_columns: number, minimumItemWidth: number, - favoritesList: FavoritesList - gridItems: UiPlugin[]; + favoritesList: FavoritesList, + uiPlugins: UiPlugin[] + //gridItems: UiPlugin[]; } @@ -207,7 +209,7 @@ export const LoadPluginDialog = grid_cell_columns: this.getCellColumns(window.innerWidth), minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220, favoritesList: this.model.favorites.get(), - gridItems: this.getFilteredPlugins("", filterType_, this.model.favorites.get()) + uiPlugins: this.model.ui_plugins.get() }; @@ -217,11 +219,12 @@ export const LoadPluginDialog = this.handleSearchStringReady = this.handleSearchStringReady.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this); + this.handlePluginsChanged = this.handlePluginsChanged.bind(this); this.requestScrollTo(); } - nominal_column_width: number = 250; + nominal_column_width: number = 350; margin_reserve: number = 30; getCellColumns(width: number) { @@ -280,23 +283,31 @@ export const LoadPluginDialog = this.setState( { favoritesList: favorites, - gridItems: this.getFilteredPlugins(null, null, favorites) }); } + handlePluginsChanged(plugins: UiPlugin[]) { + this.setState( + { + uiPlugins: plugins + } + ); + } componentDidMount() { super.componentDidMount(); this.updateWindowSize(); window.addEventListener('resize', this.updateWindowSize); + this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged); - let favorites = this.model.favorites.get(); + this.model.ui_plugins.addOnChangedHandler(this.handlePluginsChanged); + this.setState({ - favoritesList: favorites, - gridItems: this.getFilteredPlugins(null, null, favorites) + favoritesList: this.model.favorites.get(), + uiPlugins: this.model.ui_plugins.get() }); - } componentWillUnmount() { + this.model.ui_plugins.removeOnChangedHandler(this.handlePluginsChanged); this.model.favorites.removeOnChangedHandler(this.handleFavoritesChanged); super.componentWillUnmount(); window.removeEventListener('resize', this.updateWindowSize); @@ -310,7 +321,6 @@ export const LoadPluginDialog = this.setState({ search_string: "", search_collapsed: true, - gridItems: this.getFilteredPlugins("", null, null) }); } this.requestScrollTo(); @@ -331,7 +341,6 @@ export const LoadPluginDialog = this.requestScrollTo(); this.setState({ filterType: filterValue, - gridItems: this.getFilteredPlugins(null, filterValue, null) }); } onClearFilter(): void { @@ -340,8 +349,7 @@ export const LoadPluginDialog = if (this.state.filterType !== value) { this.requestScrollTo(); this.setState({ - filterType: value, - gridItems: this.getFilteredPlugins(null, value, null) + filterType: value }); } } @@ -480,7 +488,7 @@ export const LoadPluginDialog = return result; } - getFilteredPlugins(searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] { + getFilteredPlugins(plugins: UiPlugin[], searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] { try { if (searchString === null) { searchString = this.state.search_string; @@ -491,7 +499,6 @@ export const LoadPluginDialog = if (favoritesList === null) { favoritesList = this.state.favoritesList; } - let plugins = this.model.ui_plugins.get(); let results: { score: number; plugin: UiPlugin }[] = []; let searchFilter = new SearchFilter(searchString); @@ -544,12 +551,14 @@ export const LoadPluginDialog = this.scrollToRequested = true; } + private cachedGridItems: UiPlugin[] = []; // retained for the scrollTo callback. + handleScrollToCallback(element: FixedSizeGrid): void { if (element) { if (this.scrollToRequested) { this.scrollToRequested = false; let position = -1; - let gridItems = this.state.gridItems; + let gridItems = this.cachedGridItems; for (let i = 0; i < gridItems.length; ++i) { if (this.state.selected_uri === gridItems[i].uri) { position = i; @@ -568,8 +577,7 @@ export const LoadPluginDialog = if (this.changedSearchString !== undefined) { this.requestScrollTo(); this.setState({ - search_string: this.changedSearchString, - gridItems: this.getFilteredPlugins(this.changedSearchString, null, null) + search_string: this.changedSearchString }); this.changedSearchString = undefined; } @@ -588,9 +596,8 @@ export const LoadPluginDialog = 2000); } - renderItem(row: number, column: number): React.ReactNode { + renderItem(gridItems: UiPlugin[], row: number, column: number): React.ReactNode { let item: number = (row) * this.gridColumnCount + (column); - let gridItems = this.state.gridItems; if (item >= gridItems.length) { return (
); } @@ -621,7 +628,7 @@ export const LoadPluginDialog = { isFavorite && (
- +
) } @@ -647,6 +654,7 @@ export const LoadPluginDialog = } gridColumnCount: number = 1; + render() { const { classes } = this.props; @@ -701,8 +709,7 @@ export const LoadPluginDialog = this.requestScrollTo(); this.setState({ search_collapsed: true, - search_string: "", - gridItems: this.getFilteredPlugins("", null, null) + search_string: "" }); } else { @@ -718,8 +725,7 @@ export const LoadPluginDialog = this.requestScrollTo(); this.setState({ search_collapsed: true, - search_string: "", - gridItems: this.getFilteredPlugins("", null, null) + search_string: "" }); } else { @@ -764,7 +770,9 @@ export const LoadPluginDialog = } let width = arg.width ?? 1; let height = arg.height ?? 1; - let gridItems = this.state.gridItems; + let gridItems = this.getFilteredPlugins( + this.state.uiPlugins, this.state.search_string, this.state.filterType, this.state.favoritesList); + this.cachedGridItems = gridItems; let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); } @@ -793,7 +801,7 @@ export const LoadPluginDialog = {(arg: { columnIndex: number, rowIndex: number, style: CSSProperties }) => (
{ - this.renderItem(arg.rowIndex, arg.columnIndex) + this.renderItem(gridItems, arg.rowIndex, arg.columnIndex) }
)} @@ -837,11 +845,15 @@ export const LoadPluginDialog = color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri ? "block" : "block"), position: "relative" }}> - { this.setFavorite(this.state.selected_uri, !isFavorite); }} - > - - + { + this.state.selected_uri !== "uri://two-play/pipedal/pedalboard#Empty" && ( + { this.setFavorite(this.state.selected_uri, !isFavorite); }} + > + + + ) + }
diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 9a90a1b..b762ceb 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -48,7 +48,14 @@ export enum State { Error, Background, Reconnecting, - ApplyingChanges + ApplyingChanges, + ReloadingPlugins +}; + +export enum ReconnectReason { + Disconnected, + LoadingSettings, + ReloadingPlugins, }; export type ControlValueChangedHandler = (key: string, value: number) => void; @@ -356,6 +363,7 @@ export class PiPedalModel //implements PiPedalModel lv2Path: string = ""; webSocket?: PiPedalSocket; + ui_plugins: ObservableProperty = new ObservableProperty([]); state: ObservableProperty = new ObservableProperty(State.Loading); @@ -426,13 +434,20 @@ export class PiPedalModel //implements PiPedalModel onSocketReconnecting(retry: number, maxRetries: number): void { if (this.visibilityState.get() === VisibilityState.Hidden) return; //if (retry !== 0) { - if (this.restartExpected) { - this.setState(State.ApplyingChanges); - this.restartExpected = false; - } else { + switch (this.reconnectReason) + { + case ReconnectReason.Disconnected: + default: this.setState(State.Reconnecting); + break; + case ReconnectReason.LoadingSettings: + this.setState(State.ApplyingChanges); + break; + case ReconnectReason.ReloadingPlugins: + this.setState(State.ReloadingPlugins); + break; + } - //} } @@ -602,10 +617,17 @@ export class PiPedalModel //implements PiPedalModel { this.showAlert(body as string); - } else { - throw new PiPedalStateError("Unrecognized message received from server: " + message); + } + else if (message = "onLv2PluginsChanging") + { + this.onLv2PluginsChanging(); } } + onLv2PluginsChanging() : void { + this.reconnectReason = ReconnectReason.ReloadingPlugins; + // this.webSocket?.reconnect(); // let the server do it for us. + + } setError(message: string): void { this.errorMessage.set(message); this.setState(State.Error); @@ -630,6 +652,11 @@ export class PiPedalModel //implements PiPedalModel } + private reconnectReason: ReconnectReason = ReconnectReason.Disconnected; + + isReloading(): boolean { + return this.state.get() !== State.Ready; + } getWebSocket(): PiPedalSocket { if (this.webSocket === undefined) { @@ -654,7 +681,7 @@ export class PiPedalModel //implements PiPedalModel this.androidHost?.setDisconnected(false); } - this.restartExpected = false; + this.reconnectReason = ReconnectReason.Disconnected; if (this.visibilityState.get() === VisibilityState.Hidden) return; // reload state, but not configuration. @@ -1689,15 +1716,9 @@ export class PiPedalModel //implements PiPedalModel this.alertMessage.set(message); } - restartExpected: boolean = false; - expectRestart() { - this.restartExpected = true; - - - } setJackSettings(jackSettings: JackChannelSelection): void { - this.expectRestart(); + this.reconnectReason = ReconnectReason.LoadingSettings; this.webSocket?.send("setJackSettings", jackSettings); } @@ -2423,7 +2444,7 @@ export class PiPedalModel //implements PiPedalModel resolve(); }); - this.restartExpected = true; + this.reconnectReason = ReconnectReason.LoadingSettings; this.webSocket?.reconnect(); // close immediately, and wait for recoonnect. return result; } @@ -2619,10 +2640,8 @@ export class PiPedalModel //implements PiPedalModel } } - reloadRequested: boolean = false; reloadPage() { - this.reloadRequested = true; // eslint-disable-next-line no-restricted-globals let url = window.location.href.split('#')[0]; window.location.href = url; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9aa027f..8b9d091 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,6 +132,7 @@ else() endif() set (PIPEDAL_SOURCES + Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp Finally.hpp diff --git a/src/Lv2Log.cpp b/src/Lv2Log.cpp index 99c1b2c..7e189be 100644 --- a/src/Lv2Log.cpp +++ b/src/Lv2Log.cpp @@ -39,18 +39,16 @@ std::string timeTag() using namespace std::chrono; - auto t = std::chrono::system_clock::now()- timeZero; + auto now = std::chrono::system_clock::now(); + std::time_t now_c = std::chrono::system_clock::to_time_t(now); + auto milliseconds = std::chrono::duration_cast(now.time_since_epoch()) % 1000; - auto hours_ = duration_cast(t).count() % 24; - auto minutes_ = duration_cast(t).count() % 60; - auto seconds_ = duration_cast(t).count() % 60; - auto milliseconds_ = duration_cast(t).count() % 1000; std::stringstream s; using namespace std; - s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_ << " "; + s << std::put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) << std::setfill('0') << milliseconds.count() << " "; return s.str(); } diff --git a/src/Lv2PluginChangeMonitor.cpp b/src/Lv2PluginChangeMonitor.cpp new file mode 100644 index 0000000..291c53a --- /dev/null +++ b/src/Lv2PluginChangeMonitor.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "Lv2PluginChangeMonitor.hpp" +#include "Lv2Log.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "PiPedalModel.hpp" + +using namespace pipedal; + +Lv2PluginChangeMonitor::Lv2PluginChangeMonitor(PiPedalModel&model) +:model(model) +{ + shutdown_eventfd = eventfd(0, 0); + monitorThread = std::make_unique([this]() { ThreadProc();}); +} + +void Lv2PluginChangeMonitor::Shutdown() +{ + if (monitorThread) + { + terminateThread = true; + monitorThread->join(); + monitorThread = nullptr; + } +} + +Lv2PluginChangeMonitor::~Lv2PluginChangeMonitor() +{ + Shutdown(); +} + +void Lv2PluginChangeMonitor::ThreadProc() +{ + using clock = std::chrono::steady_clock; + + int inotify_fd = inotify_init(); + if (inotify_fd == -1) { + Lv2Log::error("Failed to initialize inotify"); + return; + } + + Finally f1 ([inotify_fd]() { + close(inotify_fd); + + }); + // Add the directory to the inotify watch list + int watch_descriptor = inotify_add_watch(inotify_fd, "/usr/lib/lv2", IN_MODIFY | IN_CREATE | IN_DELETE); + if (watch_descriptor == -1) { + Lv2Log::error("Failed to add directory to inotify watch list"); + return; + } + Finally f2([inotify_fd,watch_descriptor]() { + inotify_rm_watch(inotify_fd, watch_descriptor); + }); + + bool updating = false; + clock::time_point updateTime; + + // Monitor for file system events + while (true) { + struct pollfd pfds[2] = { + {.fd = inotify_fd, .events = POLLIN}, + {.fd = shutdown_eventfd, .events = POLLIN} + }; + int ret = poll(pfds, 2,500); // infinite wait + if (ret == -1) { + Lv2Log::error("Error in poll()"); + break; + } + if (ret == 0) + { + // timeout. + if (updating && clock::now() >= updateTime) + { + updating = false; + model.OnLv2PluginsChanged(); + } + continue; + } + if (pfds[1].revents & POLLIN) { + // Shutdown event received + break; + } + + char buffer[4096]; + ssize_t num_bytes = read(inotify_fd, buffer, sizeof(buffer)); + if (num_bytes == -1) { + Lv2Log::error("Error reading from inotify"); + break; + } + + size_t i = 0; + bool updated = false; + while (i < static_cast(num_bytes)) { + struct inotify_event* event = reinterpret_cast(&buffer[i]); + if (event->len > 0) { + if (event->mask & IN_MODIFY) { + updated = true; + } else if (event->mask & IN_CREATE) { + updated = true; + } else if (event->mask & IN_DELETE) { + updated = true; + } + } + i += sizeof(struct inotify_event) + event->len; + } + if (updated) + { + updating = true; + updateTime = clock::now() + std::chrono::duration_cast(std::chrono::seconds(5)); + } + } + + // Clean up +} + + + + diff --git a/src/Lv2PluginChangeMonitor.hpp b/src/Lv2PluginChangeMonitor.hpp new file mode 100644 index 0000000..8199efc --- /dev/null +++ b/src/Lv2PluginChangeMonitor.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include +#include + +namespace pipedal +{ + class PiPedalModel; + class Lv2PluginChangeMonitor { + public: + Lv2PluginChangeMonitor(PiPedalModel&model); + ~Lv2PluginChangeMonitor(); + void Shutdown(); + private: + void ThreadProc(); + + int shutdown_eventfd; + bool isClosed = false; + PiPedalModel&model; + std::unique_ptr monitorThread; + std::atomic terminateThread {false}; + }; +} \ No newline at end of file diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 1c5eed8..5972714 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -18,6 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "pch.h" +#include #include "ServiceConfiguration.hpp" #include "AudioConfig.hpp" #include "ConfigUtil.hpp" @@ -33,6 +34,7 @@ #include "RingBufferReader.hpp" #include "PiPedalUI.hpp" #include "atom_object.hpp" +#include "Lv2PluginChangeMonitor.hpp" #ifndef NO_MLOCK #include @@ -61,8 +63,8 @@ static std::string BytesToHex(const std::vector &bytes) } PiPedalModel::PiPedalModel() - : lv2Host(), - atomConverter(lv2Host.GetMapFeature()) + : pluginHost(), + atomConverter(pluginHost.GetMapFeature()) { this->pedalboard = Pedalboard::MakeDefault(); #if JACK_HOST @@ -98,6 +100,7 @@ void PiPedalModel::Close() PiPedalModel::~PiPedalModel() { + pluginChangeMonitor = nullptr; try { adminClient.UnmonitorGovernor(); @@ -133,12 +136,14 @@ PiPedalModel::~PiPedalModel() void PiPedalModel::Init(const PiPedalConfiguration &configuration) { + std::lock_guard lock(mutex); // prevent callbacks while we're initializing. + this->configuration = configuration; - lv2Host.SetConfiguration(configuration); + pluginHost.SetConfiguration(configuration); storage.SetConfigRoot(configuration.GetDocRoot()); storage.SetDataRoot(configuration.GetLocalStoragePath()); storage.Initialize(); - lv2Host.SetPluginStoragePath(storage.GetPluginUploadDirectory()); + pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory()); this->systemMidiBindings = storage.GetSystemMidiBindings(); @@ -157,7 +162,7 @@ void PiPedalModel::LoadLv2PluginInfo() { if (!std::filesystem::exists(pluginClassesPath)) throw PiPedalException("File not found."); - lv2Host.LoadPluginClassesFromJson(pluginClassesPath); + pluginHost.LoadPluginClassesFromJson(pluginClassesPath); } catch (const std::exception &e) { @@ -166,18 +171,19 @@ void PiPedalModel::LoadLv2PluginInfo() throw PiPedalException(s.str().c_str()); } - lv2Host.Load(configuration.GetLv2Path().c_str()); + pluginChangeMonitor = std::make_unique(*this); + pluginHost.Load(configuration.GetLv2Path().c_str()); // Copy all presets out of Lilv data to json files // so that we can close lilv while we're actually // running. - for (const auto &plugin : lv2Host.GetPlugins()) + for (const auto &plugin : pluginHost.GetPlugins()) { if (plugin->has_factory_presets()) { if (!storage.HasPluginPresets(plugin->uri())) { - PluginPresets pluginPresets = lv2Host.GetFactoryPluginPresets(plugin->uri()); + PluginPresets pluginPresets = pluginHost.GetFactoryPluginPresets(plugin->uri()); storage.SavePluginPresets(plugin->uri(), pluginPresets); } } @@ -191,7 +197,7 @@ void PiPedalModel::Load() adminClient.MonitorGovernor(storage.GetGovernorSettings()); - // lv2Host.Load(configuration.GetLv2Path().c_str()); + // pluginHost.Load(configuration.GetLv2Path().c_str()); this->pedalboard = storage.GetCurrentPreset(); // the current *saved* preset. @@ -211,7 +217,7 @@ void PiPedalModel::Load() } UpdateDefaults(&this->pedalboard); - std::unique_ptr p{AudioHost::CreateInstance(lv2Host.asIHost())}; + std::unique_ptr p{AudioHost::CreateInstance(pluginHost.asIHost())}; this->audioHost = std::move(p); this->audioHost->SetNotificationCallbacks(this); @@ -270,7 +276,7 @@ void PiPedalModel::Load() JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); selection = selection.RemoveInvalidChannels(jackConfiguration); - this->lv2Host.OnConfigurationChanged(jackConfiguration, selection); + this->pluginHost.OnConfigurationChanged(jackConfiguration, selection); try { audioHost->Open(this->jackServerSettings, selection); @@ -380,7 +386,6 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) item->stateUpdateCount(item->stateUpdateCount() + 1); - IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; Lv2PluginState newState = item->lv2State(); @@ -392,7 +397,7 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId) { for (size_t i = 0; i < n; ++i) { - t[i]->OnLv2StateChanged(instanceId,newState); + t[i]->OnLv2StateChanged(instanceId, newState); } } delete[] t; @@ -1151,7 +1156,6 @@ void PiPedalModel::RestartAudio() // do a complete reload. - this->audioHost->SetPedalboard(nullptr); this->jackConfiguration.AlsaInitialize(this->jackServerSettings); @@ -1183,7 +1187,7 @@ void PiPedalModel::RestartAudio() } this->audioHost->Open(this->jackServerSettings, channelSelection); - this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); + this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); std::vector errorMessages; @@ -1205,7 +1209,7 @@ void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSe std::lock_guard lock(mutex); // copy atomically. this->storage.SetJackChannelSelection(channelSelection); - this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); + this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); } RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. @@ -1283,7 +1287,7 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f } else { - pPluginInfo = lv2Host.GetPluginInfo(item->uri()); + pPluginInfo = pluginHost.GetPluginInfo(item->uri()); } if (pPluginInfo) { @@ -1475,7 +1479,7 @@ void PiPedalModel::SendSetPatchProperty( } }}; - LV2_URID urid = this->lv2Host.GetLv2Urid(propertyUri.c_str()); + LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str()); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, @@ -1536,7 +1540,7 @@ void PiPedalModel::SendGetPatchProperty( } }}; - LV2_URID urid = this->lv2Host.GetLv2Urid(uri.c_str()); + LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str()); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, clientId, instanceId, urid, onSuccess, onError); @@ -1669,7 +1673,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet FireJackConfigurationChanged(this->jackConfiguration); // restart the pedalboard on a new instance. - std::shared_ptr lv2Pedalboard{this->lv2Host.CreateLv2Pedalboard(this->pedalboard)}; + std::shared_ptr lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard)}; this->lv2Pedalboard = lv2Pedalboard; audioHost->SetPedalboard(lv2Pedalboard); @@ -1684,7 +1688,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem) { - std::shared_ptr pPlugin = lv2Host.GetPluginInfo(pedalboardItem->uri()); + std::shared_ptr pPlugin = pluginHost.GetPluginInfo(pedalboardItem->uri()); if (!pPlugin) { if (pedalboardItem->uri() == SPLIT_PEDALBOARD_ITEM_URI) @@ -1817,19 +1821,19 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); } - -void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom*atomValue) +void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) { std::lock_guard lock(mutex); - std::string propertyUri = lv2Host.GetMapFeature().UridToString(patchSetProperty); + std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); { PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); - if (item == nullptr) return; - atom_object atomObject { atomValue }; - - PedalboardItem::PropertyMap& properties = item->PatchProperties(); + if (item == nullptr) + return; + atom_object atomObject{atomValue}; + + PedalboardItem::PropertyMap &properties = item->PatchProperties(); if (properties.contains(propertyUri)) { if (properties[propertyUri] == atomObject) @@ -1924,25 +1928,25 @@ void PiPedalModel::MonitorPatchProperty(int64_t clientId, int64_t clientHandle, LV2_URID propertyUrid = 0; if (propertyUri.length() != 0) { - propertyUrid = lv2Host.GetMapFeature().GetUrid(propertyUri.c_str()); + propertyUrid = pluginHost.GetMapFeature().GetUrid(propertyUri.c_str()); } AtomOutputListener listener{clientId, clientHandle, instanceId, propertyUrid}; atomOutputListeners.push_back(listener); audioHost->SetListenForAtomOutput(true); - PedalboardItem*item = this->pedalboard.GetItem(instanceId ); + PedalboardItem *item = this->pedalboard.GetItem(instanceId); if (item) { - auto& map = item->PatchProperties(); + auto &map = item->PatchProperties(); if (map.contains(propertyUri)) { - const auto&value = map[propertyUri]; + const auto &value = map[propertyUri]; std::string json = this->audioHost->AtomToJson(value.get()); - for (auto &subscriber: this->subscribers) + for (auto &subscriber : this->subscribers) { if (subscriber->GetClientId() == clientId) { - subscriber->OnNotifyPatchProperty(clientHandle,instanceId,propertyUri,json); + subscriber->OnNotifyPatchProperty(clientHandle, instanceId, propertyUri, json); } } } @@ -2040,11 +2044,11 @@ std::vector PiPedalModel::GetFileList(const UiFileProperty &filePro return std::vector(); // don't disclose to users what the problem is. } } -std::vector PiPedalModel::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +std::vector PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) { try { - return this->storage.GetFileList2(relativePath,fileProperty); + return this->storage.GetFileList2(relativePath, fileProperty); } catch (const std::exception &e) { @@ -2054,12 +2058,12 @@ std::vector PiPedalModel::GetFileList2(const std::string &relativePat } std::string PiPedalModel::RenameFilePropertyFile( - const std::string&oldRelativePath, - const std::string&newRelativePath, - const UiFileProperty&uiFileProperty) + const std::string &oldRelativePath, + const std::string &newRelativePath, + const UiFileProperty &uiFileProperty) { std::lock_guard lock(mutex); - return storage.RenameFilePropertyFile(oldRelativePath,newRelativePath,uiFileProperty); + return storage.RenameFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty); } void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) @@ -2068,23 +2072,20 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) storage.DeleteSampleFile(fileName); } -std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty) +std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty) { std::lock_guard lock(mutex); return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); - } -FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) +FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty) { std::lock_guard lock(mutex); return storage.GetFilePropertydirectoryTree(uiFileProperty); - } - -std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream&stream, size_t contentLength) +std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) { - return storage.UploadUserFile(directory, patchProperty, filename, stream,contentLength); + return storage.UploadUserFile(directory, patchProperty, filename, stream, contentLength); } uint64_t PiPedalModel::CreateNewPreset() @@ -2096,19 +2097,18 @@ uint64_t PiPedalModel::CreateNewPreset() void PiPedalModel::CheckForResourceInitialization(Pedalboard &pedalboard) { - for (auto item: pedalboard.GetAllPlugins()) + for (auto item : pedalboard.GetAllPlugins()) { if (!item->isSplit()) { - lv2Host.CheckForResourceInitialization(item->uri(),storage.GetPluginUploadDirectory()); + pluginHost.CheckForResourceInitialization(item->uri(), storage.GetPluginUploadDirectory()); } - } } bool PiPedalModel::LoadCurrentPedalboard() { Lv2PedalboardErrorList errorMessages; - std::shared_ptr lv2Pedalboard{this->lv2Host.CreateLv2Pedalboard(this->pedalboard, errorMessages)}; + std::shared_ptr lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard, errorMessages)}; this->lv2Pedalboard = lv2Pedalboard; // apply the error messages to the lv2Pedalboard. @@ -2138,4 +2138,37 @@ void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::strin std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const { return storage.GetPluginUploadDirectory(); -} \ No newline at end of file +} + +void PiPedalModel::OnLv2PluginsChanged() +{ + Lv2Log::info("Lv2 plugins have changed. Reloading plugins."); + std::lock_guard lock(mutex); + { + // Notify clients. + size_t n = subscribers.size(); + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[n]; + for (size_t i = 0; i < n; ++i) + { + t[i] = this->subscribers[i]; + } + for (size_t i = 0; i < n; ++i) + { + t[i]->OnLv2PluginsChanging(); + } + delete[] t; + } + std::thread( + [this]() + { + // wait for the message to propagate. It would be better to use some kind of flush() + // operation, but it's not clear how to do that with asyncio. + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + restartListener(); + }) + .detach(); +} +void PiPedalModel::SetRestartListener(std::function &&listener) +{ + this->restartListener = std::move(listener); +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 95bdba4..a9aa49b 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -46,6 +46,7 @@ namespace pipedal struct RealtimeMidiProgramRequest; struct RealtimeNextMidiProgramRequest; + class Lv2PluginChangeMonitor; class IPiPedalModelSubscriber { @@ -78,12 +79,16 @@ namespace pipedal virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) = 0; //virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; virtual void OnErrorMessage(const std::string&message) = 0; + virtual void OnLv2PluginsChanging() = 0; virtual void Close() = 0; }; class PiPedalModel : private IAudioHostCallbacks { private: + std::function restartListener; + + std::unique_ptr pluginChangeMonitor; std::unique_ptr pingThread; @@ -123,8 +128,8 @@ namespace pipedal std::vector atomOutputListeners; JackServerSettings jackServerSettings; - PluginHost lv2Host; - AtomConverter atomConverter; // must be AFTER lv2Host! + PluginHost pluginHost; + AtomConverter atomConverter; // must be AFTER pluginHost! Pedalboard pedalboard; Storage storage; @@ -146,7 +151,6 @@ namespace pipedal void UpdateDefaults(PedalboardItem *pedalboardItem); void UpdateDefaults(Pedalboard *pedalboard); - class VuSubscription { public: @@ -199,6 +203,8 @@ namespace pipedal std::filesystem::path GetPluginUploadDirectory() const; void Close(); + void SetRestartListener(std::function &&listener); + void OnLv2PluginsChanged(); void SetOnboarding(bool value); void UpdateDnsSd(); @@ -210,7 +216,7 @@ namespace pipedal void LoadLv2PluginInfo(); void Load(); - const PluginHost &GetLv2Host() const { return lv2Host; } + const PluginHost &GetLv2Host() const { return pluginHost; } Pedalboard GetCurrentPedalboardCopy() { std::lock_guard guard(mutex); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 4c6cadb..ac5b6b7 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1587,6 +1587,12 @@ private: Send("onLv2StateChanged",message); } + + virtual void OnLv2PluginsChanging() override { + Send("onLv2PluginsChanging",true); + Flush(); + } + virtual void OnErrorMessage(const std::string&message) { Send("onErrorMessage",message); @@ -1778,6 +1784,9 @@ private: } } + void Flush() { + + } int outstandingNotifyAtomOutputs = 0; class PendingNotifyAtomOutput diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index b069caf..fb28e0b 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -1470,6 +1470,7 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con } } } + // void PiPedalHostLogError(const std::string &error) // { // Lv2Log::error("%s",error.c_str()); diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 0ba6c3a..9ffa5ef 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -813,6 +813,7 @@ namespace pipedal public: virtual MapFeature &GetMapFeature() { return this->mapFeature; } void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory); + void ReloadPlugins(); private: diff --git a/src/main.cpp b/src/main.cpp index 7ea3fa7..b9d8833 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,19 +47,13 @@ #include - using namespace pipedal; - - - #ifdef __ARM_ARCH_ISA_A64 #define AARCH64 #endif -sem_t signalSemaphore; - bool HasAlsaDevice(const std::vector devices, const std::string &deviceId) { for (auto &device : devices) @@ -77,21 +71,16 @@ public: std::string message(int ev) const { return "error message"; } }; -static volatile bool g_SigBreak = false; -std::atomic ga_SigBreak { false }; +static std::atomic g_SigBreak = false; +static std::atomic g_restart = false; void sig_handler(int signo) { - if (!g_SigBreak) - { - g_SigBreak = true; - sem_post(&signalSemaphore); - } + // we're using sig_wait. No need to do anything. } void throwSystemError(int error) { - } static bool isJackServiceRunning() @@ -104,8 +93,6 @@ static bool isJackServiceRunning() int main(int argc, char *argv[]) { - sem_init(&signalSemaphore, 0, 0); - #ifndef WIN32 umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction. @@ -176,7 +163,6 @@ int main(int argc, char *argv[]) } SetThreadName("main"); - std::filesystem::path doc_root = parser.Arguments()[0]; std::filesystem::path web_root = doc_root; if (parser.Arguments().size() >= 2) @@ -194,7 +180,7 @@ int main(int argc, char *argv[]) std::stringstream s; s << "Unable to read configuration from '" << (doc_root / "config.json") << "'. (" << e.what() << ")"; Lv2Log::error(s.str()); - return EXIT_FAILURE; + return EXIT_SUCCESS; // indicate to systemd that we don't want a restart. } Lv2Log::log_level(configuration.GetLogLevel()); @@ -214,7 +200,7 @@ int main(int argc, char *argv[]) auto const threads = std::max(1, configuration.GetThreads()); server = WebServer::create( - address, port, web_root.c_str(), threads,configuration.GetMaxUploadSize()); + address, port, web_root.c_str(), threads, configuration.GetMaxUploadSize()); Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads); @@ -225,7 +211,7 @@ int main(int argc, char *argv[]) std::stringstream s; s << "Fatal error: " << e.what() << std::endl; Lv2Log::error(s.str()); - return EXIT_FAILURE; + return EXIT_SUCCESS; // indiate to systemd that we don't want a restart. } try @@ -233,15 +219,25 @@ int main(int argc, char *argv[]) { auto locale = Locale::GetInstance(); Lv2Log::info(SS("Locale: " << locale->CurrentLocale())); - try { + try + { auto collator = locale->GetCollator(); - } catch (std::exception&e) + } + catch (std::exception &e) { Lv2Log::error(e.what()); - return EXIT_SUCCESS; //tell systemd not to auto-restart. + return EXIT_SUCCESS; // tell systemd not to auto-restart. } } PiPedalModel model; + + model.SetRestartListener( + []() + { + g_restart = true; + raise(SIGTERM); // throws an exception under gdb, but correctly restarts the service when running live. + }); + model.Init(configuration); // Get heavy IO out of the way before letting dependent (Jack/ALSA) services run. @@ -254,10 +250,8 @@ int main(int argc, char *argv[]) (unsigned long)getpid()); } - auto serverSettings = model.GetJackServerSettings(); - { // Wait for selected audio device to be initialized. // It may take some time for ALSA to publish all available devices when rebooting. @@ -270,7 +264,8 @@ int main(int argc, char *argv[]) if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice())) { Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); - } else + } + else { for (int i = 0; i < 5; ++i) { @@ -292,13 +287,16 @@ int main(int argc, char *argv[]) if (found) { Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << ".")); - } else { + } + else + { Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found.")); } } - } else { + } + else + { Lv2Log::info("No ALSA device selected."); - } // pre-cache device info before we let audio services run. @@ -360,7 +358,7 @@ int main(int argc, char *argv[]) server->AddSocketFactory(pipedalSocketFactory); - ConfigureWebServer(*server,model,port,configuration.GetMaxUploadSize()); + ConfigureWebServer(*server, model, port, configuration.GetMaxUploadSize()); { server->RunInBackground(-1); @@ -368,24 +366,9 @@ int main(int argc, char *argv[]) model.UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. SetThreadName("main"); - // AARCH64 sem_wait pins CPU 100%. - // static_assert(std::atomic::is_always_lock_free); - - // while (true) - // { - // auto sigBreak = ga_SigBreak.load(); - // if (sigBreak) - // { - // break; - // } - // std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - // } - - { - sigwait(&sigSet,&sig); - + sigwait(&sigSet, &sig); + if (systemd) { sd_notify(0, "STOPPING=1"); @@ -395,12 +378,14 @@ int main(int argc, char *argv[]) Lv2Log::info("Closing audio session."); model.Close(); - + Lv2Log::info("Stopping web server."); server->ShutDown(5000); server->Join(); Lv2Log::info("Shutdown complete."); + + if (g_restart) return EXIT_FAILURE; // indicate to systemd that we want a restart. } catch (const std::exception &e) { diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template index 3fe571a..0b7f588 100644 --- a/src/templates/pipedald.service.template +++ b/src/templates/pipedald.service.template @@ -16,7 +16,7 @@ User=pipedal_d Group=pipedal_d Restart=always TimeoutStartSec=60 -RestartSec=25 +RestartSec=5 TimeoutStopSec=15 WorkingDirectory=/var/pipedal diff --git a/todo.txt b/todo.txt index b68f42a..6d6b6b5 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,6 @@ +- revise docs to remove direct reference of RPI 4 (index.html) +- change icons on gallery in index.html +- provide direct link to docs on both landing pages. - feature: redo ./makepackage so that it runs without sudo. - Bug: text Cursor hovering over the breadcrumb bar in file property dialog. - Bug: Turning off wifi-direct sould re-enable NetworkManager.