diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index d689338..66490c3 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -42,7 +42,7 @@ a lv2:Plugin , - lv2:AmplifierPlugin ; + lv2:SimulatorPlugin ; doap:name "TooB Cab Simulator" , "TooB Cab Simulator"@en-gb ; diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 736e19c..41b1fb7 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -52,7 +52,7 @@ inputStage:filterGroup a lv2:Plugin , - lv2:MixerPlugin ; + lv2:SimulatorPlugin ; doap:name "TooB Input Stage" , "TooB Input Stage"@en-gb ; @@ -118,7 +118,8 @@ use the following plugins in the following order. ] , [ rdfs:label "-INF" ; rdf:value -60.0 - ] + ]; + rdfs:comment "Input level trim" ], [ a lv2:InputPort , lv2:ControlPort ; @@ -154,7 +155,7 @@ use the following plugins in the following order. rdfs:label "OFF" ; rdf:value 0.0 ]; - rdfs:comment "Boost high frequencies." ; + rdfs:comment "High frequency boost. Zero to disable." ; ], [ a lv2:InputPort , @@ -173,7 +174,7 @@ use the following plugins in the following order. rdfs:label "OFF" ; rdf:value 0.0 ]; - rdfs:comment "Bright frequency." ; + rdfs:comment "Bright cutoff frequency." ; ], [ a lv2:InputPort , diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl index 0563734..d929a9b 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl @@ -60,7 +60,7 @@ pstage:stage3 a lv2:Plugin , - lv2:AmplifierPlugin ; + lv2:SimulatorPlugin ; doap:name "TooB Power Stage"; @@ -79,8 +79,8 @@ pstage:stage3 rdfs:comment """ The TooB Power Stage provides flexible and nuanced emulations of guitar amp power stages. -The goal of TooB Power Stage is not to inflexibly emulate existing tube power stages, but instead, -to be better. +The goal of TooB Power Stage is emulate the best features of real tube amps without slavishly +reproducing the undesirable features as well. TooB Power Stage provides 3 consecutive gain stages, each of which can be overdriven to varying degrees. Power transformer sag is emulated using the SAG and SAGD controls, which emulate the @@ -105,19 +105,22 @@ General experience seems to suggest that gradually increasing the gain in each s the signal harder without introducing undesirable effects in the output. However, there definitely interesting and subtle differences in tone that can be acheived by not adhering to that rule of thumb. -The SAG and SAGD controls are important controls. You need to undertand how they work. SAG controls how much +The SAG and SAGD controls the emulation of "forgiveness", and "compression" that are provided in a real tube +amp by sagging of power supply voltage. SAG controls how much the overall volume of the output decreases as the transform sags. It provides the subtle "compression" that tube amps provide, which is quite different from the compression provided by compressors. SAGD controls the amount of additional distortion that occurs as the gain stages receive reduced voltage. It provides -the "forgiveness" of a good tube amp. Specifically, because SAGD operates in a feedback loop that runs through +the "forgiveness" of a good tube amp. Because SAGD operates in a feedback loop that runs through all three gain stages. Increasing the distortion in the gain stages increases the power use of the gain stages; increasing the power use of the gain stages increases the effect of SAG and SAGD which has an opposing effect -on power use. The net result is that using SAG and SAGD causes signals to break up more gradually +on power use. The net result is that SAGD causes signals to break up more gradually (hence the perceived "forgiveness"). The effects are non-linear, so not easy to describe completely. You -are strongly urged to experiment with SAG and SAGD settings in order to get best tone results. +are strongly urged to experiment with SAG and SAGD settings in order to get best tone results. Use very +moderate settings to emulate tube amp output; higher settings of SAG and SAGD will cause audible compression +and pumping of the outputs which may or may not be desireable. The TooB Power Stage design is based on an experimental design, that unexpectedly produced extraordinarily -good amp tones. To be perfectly honest, we have not completely explored the sonic possibilities of the +good amp tones. We have not completely explored the sonic possibilities of the TooB Power Stage. What's known so far: it produces exceptionally pretty clean tones; it can be driven really hard, and seems to preserve an unusual amount of detail in the signal even when being driven hard; and it can definitely produce signals that have many of the most prized features of tube amplifiers: warmth, detail, @@ -235,10 +238,11 @@ use the following plugins in the following order. lv2:index 5 ; lv2:symbol "bias1" ; - lv2:name "Bias1"; + lv2:name "Bias"; lv2:default 0.0 ; lv2:minimum -2.0 ; lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 1)" ], @@ -345,10 +349,12 @@ use the following plugins in the following order. lv2:index 11 ; lv2:symbol "bias2" ; - lv2:name "Bias2"; + lv2:name "Bias"; lv2:default 0.0 ; lv2:minimum -2.0 ; lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 2)" + ], [ a lv2:InputPort , @@ -466,10 +472,12 @@ use the following plugins in the following order. lv2:index 18 ; lv2:symbol "bias3" ; - lv2:name "Bias3"; + lv2:name "Bias"; lv2:default 0.0 ; lv2:minimum -2.0 ; lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 3)" + ], diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index a3db68b..042818a 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -42,7 +42,7 @@ a lv2:Plugin , - lv2:AmplifierPlugin ; + lv2:SimulatorPlugin ; doap:name "TooB Tone Stack" , "TooB Tone Stack"@en-gb ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 index dc4c8bd..207df41 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index b38a479..b636728 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ lv2:minorVersion 0 ; lv2:microVersion 13 ; rdfs:comment """ -Digital delay. +A straighforward no-frills digital delay. """ ; @@ -62,6 +62,7 @@ Digital delay. lv2:maximum 1200; units:unit units:ms; lv2:portProperty ; + rdfs:comment "Delay in ms"; ], [ @@ -76,6 +77,8 @@ Digital delay. lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc; + rdfs:comment "Attenuation of the first delay"; + ], [ a lv2:InputPort , lv2:ControlPort ; @@ -89,6 +92,7 @@ Digital delay. lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc; + rdfs:comment "Attenuation of subequent delays"; ], [ a lv2:AudioPort , diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index c715b04..74fa372 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -40,7 +40,7 @@ toobml:filterGroup a lv2:Plugin , - lv2:AmplifierPlugin ; + lv2:SimulatorPlugin ; doap:name "TooB ML Amplifier" , "TooB ML Amplifier"@en-gb ; @@ -84,7 +84,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:minimum -30.0 ; lv2:maximum 30.0 ; units:unit units:db ; - rdf:comment "Input volume trim (pre-model)"; + rdfs:comment "Input volume trim (pre-model)"; ], [ a lv2:InputPort, lv2:ControlPort ; @@ -228,6 +228,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co rdfs:label "XoticCompSP"; rdf:value 32.0 ]; + rdfs:comment "Select neural network model." ] , [ a lv2:InputPort , @@ -239,7 +240,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:default 3.0 ; lv2:minimum 0.0 ; lv2:maximum 10.0 ; - rdf:comment "Model gain (used by some but not all models)" + rdfs:comment "Model gain parameter (used by some but not all models)" ], [ a lv2:InputPort , @@ -252,7 +253,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:minimum -30.0 ; lv2:maximum 30.0 ; units:unit units:db ; - rdf:comment "Output volume trim (pot-model)"; + rdfs:comment "Output volume trim (pre-model)"; ], [ @@ -269,7 +270,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:minimum 0.0 ; lv2:maximum 1.0 ; - rdf:comment "Bass eq (pre-model)"; + rdfs:comment "Bass eq (pre-model)"; ], [ a lv2:InputPort , lv2:ControlPort ; @@ -285,9 +286,11 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:maximum 1.0 ; - rdf:comment "Mid eq (pre-model)"; + rdfs:comment "Mid eq (pre-model)"; - ], + ], + + [ a lv2:InputPort , lv2:ControlPort ; @@ -303,7 +306,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co lv2:maximum 1.0 ; - rdf:comment "Treble eq (pre-model)"; + rdfs:comment "Treble eq (pre-model)"; ], [ a lv2:OutputPort , @@ -320,16 +323,42 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 8 ; + lv2:symbol "sag" ; + lv2:name "Sag Vol"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + rdfs:comment "Transformer sag (volume)." ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 9 ; + lv2:symbol "sagd" ; + lv2:name "Sag Dist"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + rdfs:comment "Transformer sag (distortion)." ; + ], + + [ a lv2:AudioPort , lv2:InputPort ; - lv2:index 8 ; + lv2:index 10 ; lv2:symbol "in" ; lv2:name "In" ], [ a lv2:AudioPort , lv2:OutputPort ; - lv2:index 9 ; + lv2:index 11 ; lv2:symbol "out" ; lv2:name "Out" ] @@ -340,7 +369,7 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co atom:bufferType atom:Sequence ; atom:supports patch:Message; lv2:designation lv2:control ; - lv2:index 10 ; + lv2:index 12 ; lv2:symbol "control" ; lv2:name "Control" ; rdfs:comment "Plugin to GUI communication" ; @@ -351,11 +380,35 @@ and uses model files from from the GuitarML Neural Pi project (https://github.co atom:bufferType atom:Sequence ; atom:supports patch:Message; lv2:designation lv2:control ; - lv2:index 11 ; + lv2:index 13 ; lv2:symbol "notify" ; lv2:name "Notify" ; rdfs:comment "Plugin to GUI communication" ; + ], + [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 13 ; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rdfs:comment "Plugin to GUI communication" ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 14 ; + lv2:symbol "sagf" ; + lv2:name "SagF"; + lv2:default 13.0 ; + lv2:minimum 5.0 ; + lv2:maximum 25.0; + rdfs:comment "Sag filter Fc." ; ] + . diff --git a/react/public/var/config.json b/react/public/var/config.json index 20919e1..80d344a 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,8 +1,8 @@ { - "socket_server_port": 8080, + "socket_server_port": 80, "socket_server_address": "*", "debug": true, "max_upload_size": 1048576, - "fakeAndroid": true, + "fakeAndroid": false, "ui_plugins": [] } \ No newline at end of file diff --git a/react/src/Lv2Plugin.tsx b/react/src/Lv2Plugin.tsx index 667c5fe..b2420fb 100644 --- a/react/src/Lv2Plugin.tsx +++ b/react/src/Lv2Plugin.tsx @@ -44,6 +44,7 @@ export class Port implements Deserializable { this.supports_midi = input.supports_midi; this.supports_time_position = input.supports_time_position; this.port_group = input.port_group; + this.comment = input.comment; return this; } @@ -73,6 +74,7 @@ export class Port implements Deserializable { supports_midi: boolean = false; supports_time_position: boolean = false; port_group: string = ""; + comment: string = ""; } export class PortGroup { @@ -221,8 +223,11 @@ export class UiControl implements Deserializable { this.scale_points = ScalePoint.deserialize_array(input.scale_points); this.port_group = input.port_group; this.units = input.units as Units; + this.comment = input.comment; this.controlType = ControlType.Dial; + + if (this.enumeration_property && this.scale_points.length === 2) { this.controlType = ControlType.Toggle; @@ -239,7 +244,6 @@ export class UiControl implements Deserializable { { this.controlType = ControlType.Select; } - return this; } @@ -271,6 +275,7 @@ export class UiControl implements Deserializable { scale_points: ScalePoint[] = []; port_group: string = ""; units: Units = Units.none; + comment: string = ""; // Return the value of the closest scale_point. clampSelectValue(value: number): number{ diff --git a/react/src/MainPage.tsx b/react/src/MainPage.tsx index 336ebb8..ae67584 100644 --- a/react/src/MainPage.tsx +++ b/react/src/MainPage.tsx @@ -503,7 +503,6 @@ export const MainPage = GetControlView(pedalBoardItem) ) } - this.setState({showMidiBindingsDialog: false} ) } diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index f6e7508..1ac315c 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -2020,14 +2020,18 @@ class PiPedalModelImpl implements PiPedalModel { let oldSettings = this.wifiDirectConfigSettings.get(); wifiDirectConfigSettings = wifiDirectConfigSettings.clone(); - if ((!oldSettings.enable) && (!wifiDirectConfigSettings.enable)) { + if ((!oldSettings.enable) && (!wifiDirectConfigSettings.enable) + && (oldSettings.hotspotName == wifiDirectConfigSettings.hotspotName) + ) { // no effective change. resolve(); return; } if (!wifiDirectConfigSettings.enable) { + let t = wifiDirectConfigSettings.hotspotName; // hotspot name can be changed when disabled because it's also the mDNS service name. wifiDirectConfigSettings = oldSettings.clone(); wifiDirectConfigSettings.enable = false; + wifiDirectConfigSettings.hotspotName = t; } else { if (wifiDirectConfigSettings.countryCode === oldSettings.countryCode && wifiDirectConfigSettings.channel === oldSettings.channel diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index bd81fac..6e6301e 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -455,7 +455,12 @@ const PluginControlView = { nodes } -
+
+ { + (!this.state.landscapeGrid) && ( +
+ ) + }
- { - controls.map((control) => ( - - + let hasComments = false; + + for (let i = 0; i < controls.length; ++i) { + if (controls[i].comment !== "") { + hasComments = true; + break; + } + } + if (hasComments) { + let trs: React.ReactElement[] = []; + for (let i = 0; i < controls.length; ++i) { + let control = controls[i]; + + trs.push(( + + + {control.name} - - )) - } - - ); + + + + {control.comment} + + + + )); + + } + return ( + + { + trs + } +
+ ); + + } else { + return ( + + { + controls.map((control) => ( + + + {control.name} + + + )) + } + + ); + } } + + const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => { let model = PiPedalModelFactory.getInstance(); @@ -205,19 +246,25 @@ const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => {
- - Author:  - {(plugin.author_homepage !== "") - ? {plugin.author_name} - : ( - plugin.author_name - ) - } - - - {ioDescription(plugin)} - +
+
+ + Author:  + {(plugin.author_homepage !== "") + ? {plugin.author_name} + : ( + plugin.author_name + ) + } + +
+
+ + {ioDescription(plugin)} + +
+
Controls: diff --git a/react/src/Utility.tsx b/react/src/Utility.tsx index adf0144..3e22bb6 100644 --- a/react/src/Utility.tsx +++ b/react/src/Utility.tsx @@ -38,7 +38,14 @@ export interface ControlEntry { }; const Utility = class { + static isLandscape(): boolean { + return window.innerWidth > window.innerHeight; + } static isTouchDevice(): boolean { + if (!this.isLandscape()) return false; + + if (window.innerHeight > 500) return false; + return (('ontouchstart' in window) && ((navigator.maxTouchPoints??0) > 0) ); } diff --git a/react/src/WifiDirectConfigDialog.tsx b/react/src/WifiDirectConfigDialog.tsx index da0cf19..dfdac9e 100644 --- a/react/src/WifiDirectConfigDialog.tsx +++ b/react/src/WifiDirectConfigDialog.tsx @@ -339,7 +339,7 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })( autoComplete="off" id="name" spellCheck="false" - label="SSID" + label="Service Name" type="text" size={this.state.controlSize} fullWidth @@ -348,11 +348,11 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })( value={this.state.name} onChange={(e) => this.setState({ name: e.target.value, nameError: false, nameErrorMessage: NBSP })} inputRef={this.refName} - disabled={!this.state.enabled} variant="filled" />
- { this.handleCountryChanged(event); }} @@ -413,17 +413,18 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })( )} {(!this.state.landscapeLayout) && ( - this.handleEnableChanged(e)} - color="secondary" - /> - )} - label="Wi-Fi Direct Hotspot" - /> -
+
+ this.handleEnableChanged(e)} + color="secondary" + /> + )} + label="Wi-Fi Direct Hotspot" + /> +
this.setState({ name: e.target.value, nameError: false, nameErrorMessage: NBSP })} inputRef={this.refName} - disabled={!this.state.enabled} variant="filled" /> -
+
+
midiStates) + { + if (midiState != nullptr) + { + midiState->Close(); + delete midiState; + } + } + midiStates.resize(0); } std::string discover_alsa_using_apps() @@ -573,9 +582,14 @@ namespace pipedal this->channelSelection = channelSelection; open = true; - - OpenMidi(jackServerSettings, channelSelection); - OpenAudio(jackServerSettings, channelSelection); + try { + OpenMidi(jackServerSettings, channelSelection); + OpenAudio(jackServerSettings, channelSelection); + } catch (const std::exception&e) + { + Close(); + throw; + } } void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) @@ -1512,7 +1526,7 @@ namespace pipedal try { int err; - for (int retry = 0; retry < 5; ++retry) + for (int retry = 0; retry < 15; ++retry) { err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); if (err == -EBUSY) @@ -1524,13 +1538,22 @@ namespace pipedal } if (err < 0) { - throw PiPedalStateException(SS(alsaDeviceName << " not found. " + throw PiPedalStateException(SS(alsaDeviceName << " playback device not found. " << "(" << snd_strerror(err) << ")")); } - err = snd_pcm_open(&captureHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); + for (int retry = 0; retry < 15; ++retry) + { + err = snd_pcm_open(&captureHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); + if (err == -EBUSY) + { + sleep(1); + continue; + } + break; + } if (err < 0) - throw PiPedalStateException(SS(alsaDeviceName << " not found.")); + throw PiPedalStateException(SS(alsaDeviceName << " capture device not found.")); if (snd_pcm_hw_params_malloc(&playbackHwParams) < 0) { diff --git a/src/BeastServer~.cpp.bak b/src/BeastServer~.cpp.bak deleted file mode 100644 index 97d2d18..0000000 --- a/src/BeastServer~.cpp.bak +++ /dev/null @@ -1,1248 +0,0 @@ -// -// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// - -//------------------------------------------------------------------------------ -// -// Example: Advanced server -// -//------------------------------------------------------------------------------ - -#include "pch.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "HtmlHelper.hpp" -#include "Lv2Log.hpp" - -#include "BeastServer.hpp" - -#include "Uri.hpp" - -using namespace pipedal; - -using tcp = boost::asio::ip::tcp; // from -namespace http = boost::beast::http; // from -namespace websocket = boost::beast::websocket; // from - -const size_t MAX_READ_SIZE = 1 * 1024 * 204; -using namespace boost; - -std::string -pipedal::last_modified(const std::filesystem::path &path) -{ - auto cPath = path.c_str(); - - struct stat fStat; - if (stat(cPath, &fStat) == 0) - { - return HtmlHelper::timeToHttpDate(fStat.st_mtim.tv_sec); - } - else - { - return HtmlHelper::timeToHttpDate(0); - } -} - -std::map extensionsToMimeType = { - {".htm", "text/html"}, - {".html", "text/html"}, - {".php", "text/html"}, - {".css", "text/css"}, - {".txt", "text/plain"}, - {".js", "application/javascript"}, - {".json", "application/json"}, - {".xml", "application/xml"}, - {".png", "image/png"}, - {".jpe", "image/jpeg"}, - {".jpeg", "image/jpeg"}, - {".jpg", "image/jpeg"}, - {".gif", "image/gif"}, - {".bmp", "image/bmp"}, - {".ico", "image/x-icon"}, - {".tiff", "image/tiff"}, - {".tif", "image/tiff"}, - {".svg", "image/svg+xml"}, - {".svgz", "image/svg+xml"}, - {".woff", "font/woff2" }, - {".woff2", "font/woff2"} -}; -// Return a reasonable mime type based on the extension of a file. -std::string -mime_type(const std::filesystem::path &path) -{ - auto const ext = path.extension(); - try - { - return extensionsToMimeType.at(ext); - } - catch (const std::exception &) - { - return "application/octet-stream"; - } -} - -// Append an HTTP rel-path to a local filesystem path. -// The returned path is normalized for the platform. -std::filesystem::path -path_cat( - boost::beast::string_view base, - const std::vector &segments) -{ - - std::filesystem::path result = base.to_string(); - - for (const std::string &segment : segments) - { - if (segment.find("..") != -1) - { - throw std::invalid_argument("Invalid segment."); - } - if (segment.find('/') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find('\\') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find('/') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find(':') != -1) - throw std::invalid_argument("Invalid segment."); - - result /= segment; - } - return result; -} - -std::string GetFromAddress(const tcp::socket &socket) -{ - std::stringstream s; - s << socket.remote_endpoint().address().to_string() << ':' << socket.remote_endpoint().port(); - return s.str(); -} -// This function produces an HTTP response for the given -// request. The type of the response object depends on the -// contents of the request, so the interface requires the -// caller to pass a generic lambda for receiving the response. -template < - class Body, class Allocator, - class Send> -void handle_request( - const tcp::socket &socket_, - BeastServer *server, - http::request> &&req, - Send &&send) -{ - - - // Returns a bad request response - auto const bad_request = - [&req, &socket_](boost::beast::string_view why) - { - Lv2Log::error("http - (%s) Bad request: %s,", GetFromAddress(socket_).c_str(), why.to_string().c_str()); - http::response res{http::status::bad_request, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - res.keep_alive(req.keep_alive()); - res.body() = why.to_string(); - res.prepare_payload(); - return res; - }; - - // Returns a not found response - auto const not_found = - [&req, &socket_](boost::beast::string_view target) - { - Lv2Log::error("http - (%s) Not found: %s,", GetFromAddress(socket_).c_str(), target.to_string().c_str()); - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - res.keep_alive(req.keep_alive()); - res.body() = "The resource '" + target.to_string() + "' was not found."; - res.prepare_payload(); - return res; - }; - - // Returns a server error response - auto const server_error = - [&req, &socket_](boost::beast::string_view what) - { - Lv2Log::error("http - (%s) Server error: %s,", GetFromAddress(socket_).c_str(), what.to_string().c_str()); - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - res.keep_alive(req.keep_alive()); - res.body() = what.to_string(); - res.prepare_payload(); - return res; - }; - - if (server->HasAccessPointGateway()) - { - auto &gateway = server->GetAccessPointGateway(); - auto remoteAddress = socket_.remote_endpoint().address(); - if (remoteAddress.is_v4()) - { - auto v4Address = remoteAddress.to_v4(); - unsigned int gwAddr = (gateway.netmask().to_uint() & gateway.address().to_uint()); - unsigned int ipAddr = (gateway.netmask().to_uint() & v4Address.to_uint()); - - if ((gateway.netmask().to_uint() & gateway.address().to_uint()) == (gateway.netmask().to_uint() & v4Address.to_uint())) - { - std::string host = req[http::field::host].to_string(); - std::string gatewayHost = server->GetAccessPointServerAddress(); - if (host != gatewayHost) - { - - uri requestUri; - requestUri.set(req.target().begin(), req.target().end()); - if (requestUri.segment_count() != 0) { - return send(not_found(req.target())); - } - uri_builder builder(requestUri); - builder.set_authority(gatewayHost); - - std::string redirectUri = builder.str(); - Lv2Log::info("Access point redirect from" + host + " to " + redirectUri); - - - // redirect to the gateway host. - http::response res{http::status::temporary_redirect, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::location, redirectUri); - res.prepare_payload(); - return send(std::move(res)); - - } - } - } - } - - if (req.method() == http::verb::options) - { - http::response res{http::status::no_content, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - res.set(http::field::access_control_allow_methods, "POST, GET, OPTIONS, HEADERS"); - res.set(http::field::access_control_allow_headers, "Content-Type"); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - - uri request_uri; - try - { - request_uri.set(req.target().begin(), req.target().end()); - std::string host = req[http::field::host].to_string(); - int pos = host.find_last_of(':'); - if (pos != std::string::npos) - { - host = host.substr(0, pos); - } - uri_builder builder(request_uri); - builder.set_authority(host); - builder.set_port(socket_.local_endpoint().port()); - // stub: set htts scheme once we implement it. - builder.set_scheme("http"); - - std::string strUri = builder.str(); - request_uri = uri(strUri.c_str()); - } - catch (const std::exception &_) - { - return send((bad_request("Malformed target."))); - } - - for (auto requestHandler : server->RequestHandlers()) - { - - if (requestHandler->wants(req.method(), request_uri)) - { - - if (req.method() == http::verb::head) - { - Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_), request_uri.str().c_str()); - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->head_response(request_uri, req, res, ec); - res.keep_alive(req.keep_alive()); - // Handle the case where the file doesn't exist - - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - if (ec) - { - return send(server_error(ec.message())); - } - req.prepare_payload(); - return send(std::move(res)); - } - else if (req.method() == http::verb::get) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->get_response(request_uri, req, res, ec); - res.keep_alive(req.keep_alive()); - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - if (ec) - { - return send(server_error(ec.message())); - } - res.prepare_payload(); - return send(std::move(res)); - } - else if (req.method() == http::verb::post) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) post %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->post_response(request_uri, req, res, ec); - res.keep_alive(false); // bug in Beast Server -- posted body not reset. - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - if (ec) - { - return send(server_error(ec.message())); - } - res.prepare_payload(); - return send(std::move(res)); - } - return send(bad_request("Unknown HTTP-method")); - } - } - // Make sure we can handle the method - if (req.method() != http::verb::get && - req.method() != http::verb::head) - return send(bad_request("Unknown HTTP-method")); - - // Request path must be absolute and not contain "..". - if (req.target().empty() || - req.target()[0] != '/' || - req.target().find("..") != boost::beast::string_view::npos) - return send(not_found(req.target())); // security: report not found to avoid disclosing info. - - // Build the path to the requested file - auto path = path_cat(server->GetDocRoot().c_str(), request_uri.segments()); - if (req.target().back() == '/' || req.target().size() == 0) - path /= "index.html"; - - // Attempt to open the file - boost::beast::error_code ec; - http::file_body::value_type body; - body.open(path.c_str(), boost::beast::file_mode::scan, ec); - - // Handle the case where the file doesn't exist - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - // Handle an unknown error - if (ec) - return send(server_error(ec.message())); - - // Respond to HEAD request - if (req.method() == http::verb::head) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::content_type, mime_type(path)); - res.set(http::field::last_modified, last_modified(path)); - res.content_length(body.size()); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - else if (req.method() == http::verb::get) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - // Respond to GET request - http::response res{ - std::piecewise_construct, - std::make_tuple(std::move(body)), - std::make_tuple(http::status::ok, req.version())}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::content_type, mime_type(path)); - res.set(http::field::last_modified, last_modified(path)); - res.content_length(body.size()); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - else - { - return send(bad_request("Unknown HTTP-method")); - } -} - -//------------------------------------------------------------------------------ - -// Echoes back all received WebSocket messages -class websocket_session : public std::enable_shared_from_this, private SocketHandler::IWriteCallback -{ - websocket::stream ws_; - boost::asio::strand< - boost::asio::io_context::executor_type> - strand_; - boost::asio::steady_timer timer_; - boost::beast::multi_buffer read_buffer_; - boost::beast::multi_buffer write_buffer_; - - BeastServer *beastServer_; - std::shared_ptr socket_handler_; - - std::queue writeQueue; - - bool writePending = false; - ; - std::mutex writeQueueMutex; - std::string fromAddress; - -public: - ~websocket_session() - { - Lv2Log::info("http - (%s) Websocket closed.", fromAddress.c_str()); - } - virtual void close() - { - this->ws_.close(websocket::close_reason()); - } - virtual std::string getFromAddress() const { - return this->fromAddress; - } - virtual void writeCallback(const std::string &text) - { - { - std::lock_guard guard(writeQueueMutex); - if (writePending) - { - writeQueue.push(text); - return; - } - writePending = true; - } - do_write(text); - } - void do_write(const std::string &text) - { - size_t n = buffer_copy(write_buffer_.prepare(text.size()), boost::asio::buffer(text)); - write_buffer_.commit(n); - ws_.text(true); - - ws_.async_write( - write_buffer_.data(), - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_write, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); - } - // Take ownership of the socket - explicit websocket_session( - const std::string &fromAddress, - BeastServer *beastServer, - std::shared_ptr &&socketHandler, - tcp::socket socket) - : beastServer_(beastServer), socket_handler_(socketHandler), - fromAddress(fromAddress), - ws_(std::move(socket)), - strand_(ws_.get_executor()), - read_buffer_(MAX_READ_SIZE), - timer_(ws_.get_executor().context(), - (std::chrono::steady_clock::time_point::max)()) - { - } - - // Start the asynchronous operation - template - void - run(http::request> req) - { - // Run the timer. The timer is operated - // continuously, this simplifies the code. - on_timer({}); - - // Set the timer - timer_.expires_after(std::chrono::seconds(15)); - // Accept the websocket handshake - - ws_.async_accept( - req, - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_accept, - shared_from_this(), - std::placeholders::_1))); - } - - // Called when the timer expires. - void - on_timer(boost::system::error_code ec) - { - if (ec && ec != boost::asio::error::operation_aborted) - this->beastServer_->onError(ec, "timer"); - return; - - // Verify that the timer really expired since the deadline may have moved. - if (timer_.expiry() <= std::chrono::steady_clock::now()) - { - // Closing the socket cancels all outstanding operations. They - // will complete with boost::asio::error::operation_aborted - ws_.next_layer().shutdown(tcp::socket::shutdown_both, ec); - ws_.next_layer().close(ec); - return; - } - - // Wait on the timer - timer_.async_wait( - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_timer, - shared_from_this(), - std::placeholders::_1))); - } - - void - on_accept(boost::system::error_code ec) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - { - beastServer_->onError(ec, "accept"); - return; - } - if (socket_handler_) - { - auto t = socket_handler_.get(); - t->setWriteCallback(this); - socket_handler_->onAttach(); - } - - // Read a message - do_read(); - } - - void - do_read() - { - // Set the timer - timer_.expires_after(std::chrono::seconds(15)); - - // Read a message into our buffer - ws_.async_read( - read_buffer_, - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_read, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); - } - - void - on_read( - boost::system::error_code ec, - std::size_t bytes_transferred) - { - - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - // This indicates that the websocket_session was closed - if (ec == websocket::error::closed) - return; - - if (ec) - { - beastServer_->onError(ec, "read"); - return; - } - - // Echo the message - if (ws_.got_text()) - { - std::string data(beast::buffers_to_string(read_buffer_.data())); - this->socket_handler_->onReceive(data); - read_buffer_.consume(bytes_transferred); - } - else - { - read_buffer_.consume(bytes_transferred); // ignore binary messages - } - - do_read(); - // ws_.text(ws_.got_text()); - // ws_.async_write( - // buffer_.data(), - // boost::asio::bind_executor( - // strand_, - // std::bind( - // &websocket_session::on_write, - // shared_from_this(), - // std::placeholders::_1, - // std::placeholders::_2))); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - void - on_write( - boost::system::error_code ec, - std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - { - fail(ec, "write"); - } - - // Clear the buffer - std::string nextWrite; - write_buffer_.consume(write_buffer_.size()); - - { - std::lock_guard guard(this->writeQueueMutex); - if (writeQueue.size() != 0) - { - nextWrite = writeQueue.front(); - writeQueue.pop(); - } - else - { - writePending = false; - return; - } - } - do_write(nextWrite); - } -}; - -// Handles an HTTP server connection -class http_session : public std::enable_shared_from_this -{ - // This queue is used for HTTP pipelining. - class queue - { - enum - { - // Maximum number of responses we will queue - limit = 8 - }; - - // The type-erased, saved work item - struct work - { - virtual ~work() = default; - virtual void operator()() = 0; - }; - - http_session &self_; - std::vector> items_; - - public: - explicit queue(http_session &self) - : self_(self) - { - static_assert(limit > 0, "queue limit must be positive"); - items_.reserve(limit); - } - - // Returns `true` if we have reached the queue limit - bool - is_full() const - { - return items_.size() >= limit; - } - - // Called when a message finishes sending - // Returns `true` if the caller should initiate a read - bool - on_write() - { - BOOST_ASSERT(!items_.empty()); - auto const was_full = is_full(); - items_.erase(items_.begin()); - if (!items_.empty()) - (*items_.front())(); - return was_full; - } - - // Called by the HTTP handler to send a response. - template - void - operator()(http::message &&msg) - { - // This holds a work item - struct work_impl : work - { - http_session &self_; - http::message msg_; - - work_impl( - http_session &self, - http::message &&msg) - : self_(self), msg_(std::move(msg)) - { - } - - void - operator()() - { - http::async_write( - self_.socket_, - msg_, - boost::asio::bind_executor( - self_.strand_, - std::bind( - &http_session::on_write, - self_.shared_from_this(), - std::placeholders::_1, - msg_.need_eof()))); - } - }; - - // Allocate and store the work - items_.emplace_back(new work_impl(self_, std::move(msg))); - - // If there was no previous work, start this one - if (items_.size() == 1) - (*items_.front())(); - } - }; - - tcp::socket socket_; - boost::asio::strand< - boost::asio::io_context::executor_type> - strand_; - boost::asio::steady_timer timer_; - boost::beast::flat_buffer buffer_; - http::request req_; - queue queue_; - BeastServer *beastServer_; - -public: - // Take ownership of the socket - explicit http_session( - BeastServer *beastServer, - tcp::socket socket) - : beastServer_(beastServer), socket_(std::move(socket)), strand_(socket_.get_executor()), timer_(socket_.get_executor().context(), - (std::chrono::steady_clock::time_point::max)()), - queue_(*this) - { - } - - // Start the asynchronous operation - void - run() - { - // Run the timer. The timer is operated - // continuously, this simplifies the code. - on_timer({}); - - do_read(); - } - void - do_read() - { - // make the request empty before reading - // otherwise the operation behavour is undefined. - req_ = {}; - - // Set the time - timer_.expires_after(std::chrono::seconds(15)); - - // Read a request - http::async_read(socket_, buffer_, req_, - boost::asio::bind_executor( - strand_, - std::bind( - &http_session::on_read, - shared_from_this(), - std::placeholders::_1))); - } - - // Called when the timer expires. - void - on_timer(boost::system::error_code ec) - { - if (ec && ec != boost::asio::error::operation_aborted) - return fail(ec, "timer"); - - // Verify that the timer really expired since the deadline may have moved. - if (timer_.expiry() <= std::chrono::steady_clock::now()) - { - // Closing the socket cancels all outstanding operations. They - // will complete with boost::asio::error::operation_aborted - if (!beastServer_->Stopping()) - { - socket_.shutdown(tcp::socket::shutdown_both, ec); - } - socket_.close(ec); - return; - } - - // Wait on the timer - timer_.async_wait( - boost::asio::bind_executor( - strand_, - std::bind( - &http_session::on_timer, - shared_from_this(), - std::placeholders::_1))); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - - void - on_read(boost::system::error_code ec) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - // This means they closed the connection - if (ec == http::error::end_of_stream) - return do_close(); - - if (ec) - { - fail(ec, "read"); - return; - } - // See if it is a WebSocket Upgrade - if (websocket::is_upgrade(req_)) - { - uri request_uri; - try - { - request_uri.set(req_.target().begin(), req_.target().end()); - } - catch (const std::exception &_) - { - fail(ec, "socket uri"); - return; - } - - std::shared_ptr socketFactory = beastServer_->GetSocketFactory(request_uri); - if (!socketFactory) - { - fail(http::error::bad_target, "Not found"); - return; - } - - Lv2Log::info("http - (%s) Websocket opened: %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - // Create a WebSocket websocket_session by transferring the socket - std::string fromName = GetFromAddress(socket_); - std::make_shared( - fromName, - this->beastServer_, - socketFactory ? socketFactory->CreateHandler(request_uri) : nullptr, - std::move(socket_)) - ->run(std::move(req_)); - return; - } - // Send the response - handle_request( - this->socket_, - beastServer_, - std::move(req_), - queue_); - - // If we aren't at the queue limit, try to pipeline another request - if (!queue_.is_full()) - do_read(); - } - - void - on_write(boost::system::error_code ec, bool close) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - return fail(ec, "write"); - - if (close) - { - // This means we should close the connection, usually because - // the response indicated the "Connection: close" semantic. - return do_close(); - } - - // Inform the queue that a write completed - if (queue_.on_write()) - { - // Read another request - do_read(); - } - } - - void - do_close() - { - // Send a TCP shutdown - boost::system::error_code ec; - if (!beastServer_->Stopping()) - { - socket_.shutdown(tcp::socket::shutdown_send, ec); - } - socket_.close(); - - - } -}; - -//------------------------------------------------------------------------------ - -// Accepts incoming connections and launches the sessions -class listener : public std::enable_shared_from_this -{ - tcp::acceptor acceptor_; - tcp::socket socket_; - BeastServer *beastServer_; - -public: - ~listener() - { - beastServer_->onListenerClosed(); - } - listener( - BeastServer *server, - boost::asio::io_context &ioc, - tcp::endpoint endpoint) - : beastServer_(server), acceptor_(ioc), socket_(ioc) - { - boost::system::error_code ec; - - // Open the acceptor - acceptor_.open(endpoint.protocol(), ec); - if (ec) - { - fail(ec, "open"); - return; - } - - boost::asio::socket_base::reuse_address option(true); - acceptor_.set_option(option); - // Lv2Log::warning("http - Accepting connections with reuse_adddress to allow faster debug turns. Should NOT be enabled in production use. "); - - // Leftover client sockets in TIME_WAIT state prevent binding of the server address. - // Retry for up to 140 seconds in order to allow the TIME_WAIT sockets to expire. - // If we still can't establish a connection, we can be sure that somebody else or an existing instance is servicing the address, so - // quit after that. - int retries = 0; - int retryTime = 1; - int totalRetryTime = 0; - int MAX_RETRY_TIME = 240; // socket-timeout-ish. - - while (true) - { - // Bind to the server address - acceptor_.bind(endpoint, ec); - if (!ec) - { - break; - } - else - { - ++retries; - if (totalRetryTime > MAX_RETRY_TIME) - { - fail(ec, "bind"); - return; - } - Lv2Log::info("http - bind: %s. (port %d) Retrying...", ec.message().c_str(), endpoint.port()); - sleep(retryTime); - retryTime *= 2; - if (retryTime > 10) - retryTime = 10; - totalRetryTime += retryTime; - } - } - - Lv2Log::info("http - Server listening on %s:%d", endpoint.address().to_string().c_str(), (int)endpoint.port()); - - // Start listening for connections - acceptor_.listen( - boost::asio::socket_base::max_listen_connections, ec); - if (ec) - { - fail(ec, "listen"); - return; - } - } - - void - stop() - { - acceptor_.cancel(); - } - - // Start accepting incoming connections - void - run() - { - if (!acceptor_.is_open()) - return; - do_accept(); - } - - void - do_accept() - { - acceptor_.async_accept( - socket_, - std::bind( - &listener::on_accept, - shared_from_this(), - std::placeholders::_1)); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - void - on_accept(boost::system::error_code ec) - { - if (ec) - { - fail(ec, "accept"); - return; - } - else - { - // Create the http_session and run it - std::make_shared( - beastServer_, - std::move(socket_)) - ->run(); - } - - // Accept another connection - do_accept(); - } -}; - -//------------------------------------------------------------------------------ - -BeastServer::BeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) - : address(address), - port((unsigned short)port), - doc_root(rootPath), - threads(threads), - pBgThread(nullptr) -{ -} - -void BeastServer::Stop() -{ - if (stopping) return; - stopping = true; - listener *p = nullptr; - { - std::unique_lock lk(io_mutex); - p = (listener*)this->pListener; - this->pListener = nullptr; - } - if (p != nullptr) - { - p->stop(); - } - boost::asio::io_context *pIoContext = nullptr; - { - std::unique_lock lk(io_mutex); - pIoContext = this->pIoContext; - this->pIoContext = nullptr; - } - if (pIoContext != nullptr) - { - pIoContext->stop(); - }; -} - -void BeastServer::onListenerClosed() -{ - std::unique_lock lk(io_mutex); - this->pListener = nullptr; -} - -static void ThreadProc(BeastServer *server) -{ - server->Run(); -} - -void BeastServer::RunInBackground() -{ - if (this->pBgThread != nullptr) - { - throw std::runtime_error("Bad state."); - } - this->pBgThread = new std::thread(ThreadProc, this); -} - -void BeastServer::Join() -{ - if (this->pBgThread != nullptr) - { - Stop(); - this->pBgThread->join(); - this->pBgThread = nullptr; - } -} - -void BeastServer::Run() -{ - // The io_context is required for all I/O - boost::asio::io_context ioc{threads}; - - { - // save a reference for use by stop() - std::unique_lock lk(io_mutex); - pIoContext = &ioc; - } - - // Create and launch a listening port - std::shared_ptr listenerInstance = std::make_shared( - this, - ioc, - tcp::endpoint{address, port}); - listenerInstance->run(); - - // save a reference for use by stop() - { - std::unique_lock lk(io_mutex); - this->pListener = (void *)&(*listenerInstance); - } - - // Run the I/O service on the requested number of threads - std::vector v; - v.reserve(threads - 1); - for (auto i = threads - 1; i > 0; --i) - v.emplace_back( - [&ioc] - { - ioc.run(); - }); - ioc.run(); - - ioc.stop(); - for (auto &thread : v) - { - thread.join(); - } - { - std::unique_lock lk(io_mutex); - this->pListener = nullptr; - this->pIoContext = nullptr; - } -} - -BeastServer::~BeastServer() -{ - // Shutdown if running in background. - Stop(); - Join(); -} - -void BeastServer::onWarning(boost::system::error_code ec, char const *what) -{ - std::stringstream s; - s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; - Lv2Log::warning(s.str().c_str()); -} -void BeastServer::onInfo(char const *message) -{ - Lv2Log::info("%s", message); -} - -void BeastServer::onError(boost::system::error_code ec, char const *what) -{ - std::stringstream s; - s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; - Lv2Log::warning(s.str().c_str()); -} - -std::shared_ptr BeastServer::GetSocketFactory(const uri &request_uri) -{ - for (auto factory : this->socket_factories) - { - if (factory->wants(request_uri)) - { - return factory; - } - } - return nullptr; -} diff --git a/src/BeastServer~.hpp.bak b/src/BeastServer~.hpp.bak deleted file mode 100644 index 74c12f2..0000000 --- a/src/BeastServer~.hpp.bak +++ /dev/null @@ -1,217 +0,0 @@ -#pragma once - - -// avoid breaking changes from 1.64 to.71 -// Note that raspbian only prvides 1.64; ubuntu studio only provides 1.71 -#define BOOST_BEAST_ALLOW_DEPRECATED - -#include -#include -#include - - -#include -#include -#include "Uri.hpp" -#include -#include - - -// forward declaration. - -class websocket_session; - -namespace pipedal { -using HttpRequest =boost::beast::http::request; - - -std::string last_modified(const std::filesystem::path& path); - -class SocketHandler { -public: - - class IWriteCallback { - public: - virtual void close() = 0; - - virtual void writeCallback(const std::string& text) = 0; - virtual std::string getFromAddress() const = 0; - }; - -private: - friend class ::websocket_session; - - - IWriteCallback *writeCallback_; - - void setWriteCallback(IWriteCallback *writeCallback) { - writeCallback_ = writeCallback; - } - -protected: - virtual void onReceive(const std::string_view&text) = 0; -public: - std::string getFromAddress() const { return writeCallback_->getFromAddress(); } - void receive(const std::string_view&text) { - onReceive(text); - } - void send(const std::string &text) { - if (writeCallback_ != nullptr) - { - writeCallback_->writeCallback(text); - } - } - virtual void Close() - { - writeCallback_->close(); - } - - virtual ~SocketHandler() = default; - - virtual void onAttach() { } - -}; - -class ISocketFactory { -public: - virtual bool wants(const uri &request) = 0; - virtual std::shared_ptr CreateHandler(const uri& request) = 0; -}; - -class RequestHandler { -private: - uri target_url_; -public: - RequestHandler(const char*target_url) - : target_url_(target_url) - { - - } - ~RequestHandler() = default; - - virtual bool wantsRedirect(const uri& requestUri) - { - return false; - } - uri GetRedirect(const uri& requestUri) - { - return requestUri; - } - - virtual bool wants(boost::beast::http::verb verb,const uri &request_uri) const { - if (request_uri.segment_count() < target_url_.segment_count()) - { - return false; - } - for (int i = 0; i < target_url_.segment_count(); ++i) - { - if (request_uri.segment(i) != target_url_.segment(i)) - { - return false; - } - } - return true; - } - - virtual void head_response( - const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) = 0; - - virtual void get_response( - const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) = 0; - - virtual void post_response( - const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) - { - get_response(request_uri,req,res,ec); - } - - -}; -class BeastServer { -private: - boost::asio::ip::address address; - unsigned short port; - std::string doc_root; - int threads; - std::mutex log_mutex; - std::vector > request_handlers; - std::vector > socket_factories; - std::mutex io_mutex; - boost::asio::io_context *pIoContext = nullptr; - void*pListener = nullptr; - std::thread*pBgThread = nullptr; - - bool logHttpRequests = false; - - bool hasAccessPointGateway = false; - boost::asio::ip::network_v4 accessPointGateway; - std::string accessPointServerAddress; - bool stopping = false; - - -public: - - BeastServer( - const boost::asio::ip::address& address, - int port, - const char*rootPath, - int threads = 10); - - void Run(); - void RunInBackground(); - bool Stopping() const { return this->stopping; } - void Stop(); - void Join(); - - void SetLogHttpRequests(bool logRequests) { this->logHttpRequests = logRequests; } - bool LogHttpRequests() const { return this->logHttpRequests; } - boost::asio::io_context& IoContext() { - return *pIoContext; - } - - void AddRequestHandler(std::shared_ptr requestHandler) - { - request_handlers.push_back(requestHandler); - } - - void AddSocketFactory(const std::shared_ptr &socketHandler) - { - socket_factories.push_back(socketHandler); - } - - std::vector > &RequestHandlers() { return request_handlers; } - - const std::string&GetDocRoot() { return doc_root; } - - virtual ~BeastServer(); - - std::shared_ptr GetSocketFactory(const uri &request_uri); - - void SetAccessPointGateway(const boost::asio::ip::network_v4 &gateway, const std::string&accessPointServerAddress) - { - this->accessPointGateway = gateway; - this->hasAccessPointGateway = true; - this->accessPointServerAddress = accessPointServerAddress; - } - bool HasAccessPointGateway() const { return hasAccessPointGateway; } - boost::asio::ip::network_v4 &GetAccessPointGateway() { return this->accessPointGateway; } - const std::string & GetAccessPointServerAddress() const { return this->accessPointServerAddress; } - -public: - virtual void onError(boost::system::error_code ec, char const* what); - virtual void onWarning(boost::system::error_code ec, char const* what); - virtual void onInfo(char const* what); - - virtual void onListenerClosed(); -}; - -} // namespace pipedal; \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a083b5..75725f0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,9 +89,10 @@ set (PIPEDAL_SOURCES CpuUse.hpp CpuUse.cpp P2pConfigFiles.hpp AvahiService.cpp AvahiService.hpp - DeviceIdFile.cpp DeviceIdFile.hpp + ServiceConfiguration.cpp ServiceConfiguration.hpp WriteTemplateFile.cpp WriteTemplateFile.hpp - + ConfigUtil.hpp ConfigUtil.cpp + ConfigSerializer.h ConfigSerializer.cpp StdErrorCapture.hpp StdErrorCapture.cpp Ipv6Helpers.cpp Ipv6Helpers.hpp PluginPreset.cpp PluginPreset.hpp @@ -236,12 +237,13 @@ add_test(NAME DevTest COMMAND pipedaltest "[Dev]") add_executable(pipedalconfig PrettyPrinter.hpp - DeviceIdFile.cpp DeviceIdFile.hpp + ServiceConfiguration.cpp ServiceConfiguration.hpp json.cpp json.hpp HtmlHelper.cpp HtmlHelper.hpp CommandLineParser.hpp WriteTemplateFile.hpp WriteTemplateFile.cpp PiPedalException.hpp + ConfigSerializer.h ConfigSerializer.cpp ConfigMain.cpp WifiConfigSettings.cpp WifiConfigSettings.hpp WifiDirectConfigSettings.cpp WifiDirectConfigSettings.hpp @@ -283,7 +285,9 @@ target_link_libraries(pipedal_latency_test PRIVATE pthread asound add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp UnixSocket.cpp UnixSocket.hpp - DeviceIdFile.cpp DeviceIdFile.hpp + ServiceConfiguration.cpp ServiceConfiguration.hpp + ConfigSerializer.h ConfigSerializer.cpp + autoptr_vector.h JackServerSettings.hpp JackServerSettings.cpp json.hpp json.cpp HtmlHelper.hpp HtmlHelper.cpp Lv2Log.hpp Lv2Log.cpp diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index e819b47..854dcb1 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -33,7 +33,7 @@ #include "JackServerSettings.hpp" #include "P2pConfigFiles.hpp" #include "PrettyPrinter.hpp" -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include #include #include "AudioConfig.hpp" @@ -366,6 +366,7 @@ void InstallLimits() } +#if JACK_HOST void MaybeStartJackService() { std::filesystem::path drcFile = "/etc/jackdrc"; @@ -379,7 +380,9 @@ void MaybeStartJackService() silentSysExec(SYSTEMCTL_BIN " mask jack"); } } -void InstallJackService() +#endif + +void InstallAudioService() { InstallLimits(); InstallPamEnv(); @@ -435,6 +438,28 @@ int SudoExec(char **argv) } } +static bool IsP2pServiceEnabled() +{ + WifiDirectConfigSettings settings; + settings.Load(); + return settings.enable_; +} + +static void RestartP2pdService() +{ + if (IsP2pServiceEnabled()) + { + silentSysExec(SYSTEMCTL_BIN " restart " PIPEDAL_P2PD_SERVICE); + } +} + +static void UninstallP2pdService() +{ + silentSysExec("systemctl stop " PIPEDAL_P2PD_SERVICE); + silentSysExec("systemctl disable " PIPEDAL_P2PD_SERVICE); +} + + void Uninstall() { try @@ -444,9 +469,13 @@ void Uninstall() StopService(); DisableService(); + UninstallP2pdService(); + +#if UNINSTALL_JACK_SERVICE silentSysExec(SYSTEMCTL_BIN " stop jack"); silentSysExec(SYSTEMCTL_BIN " disable jack"); - OnWifiUninstall(); +#endif + try { @@ -545,33 +574,33 @@ static std::string RandomChars(int nchars) return s.str(); } -static void PrepareDeviceidFile() +static void PrepareServiceConfigurationFile(uint16_t portNumber) { - if (!std::filesystem::exists(DeviceIdFile::DEVICEID_FILE_NAME)) + ServiceConfiguration serviceConfiguration; + try { + serviceConfiguration.Load(); + } catch (const std::exception &) { - DeviceIdFile deviceIdFile; - - deviceIdFile.deviceName = "PiPedal"; - deviceIdFile.uuid = MakeUuid(); - deviceIdFile.Save(); - } else { - DeviceIdFile deviceIdFile; - if (deviceIdFile.deviceName == "" || deviceIdFile.uuid == "") - { - if (deviceIdFile.deviceName == "") - { - deviceIdFile.deviceName = "PiPedal"; - } - if (deviceIdFile.uuid == "") - { - deviceIdFile.uuid = MakeUuid(); - } - deviceIdFile.Save(); - } } + if (serviceConfiguration.deviceName == "" || serviceConfiguration.uuid == "" + || portNumber != serviceConfiguration.server_port + ) + { + if (serviceConfiguration.deviceName == "") + { + serviceConfiguration.deviceName = "PiPedal"; + } + if (serviceConfiguration.uuid == "") + { + serviceConfiguration.uuid = MakeUuid(); + } + serviceConfiguration.server_port = portNumber; + serviceConfiguration.Save(); + } } + void Install(const std::filesystem::path &programPrefix, const std::string endpointAddress) { if (sysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS) @@ -583,8 +612,7 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo throw PiPedalException("Failed to create pipedald service group."); } - PrepareDeviceidFile(); - InstallJackService(); + InstallAudioService(); auto endpos = endpointAddress.find_last_of(':'); if (endpos == string::npos) { @@ -610,6 +638,9 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo s << "Invalid port number: " << strPort; throw PiPedalException(s.str()); } + PrepareServiceConfigurationFile(port); + + bool authBindRequired = port < 512; @@ -744,6 +775,8 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo RestartService(false); EnableService(); + RestartP2pdService(); + cout << "Complete" << "\n"; } diff --git a/src/ConfigSerializer.cpp b/src/ConfigSerializer.cpp new file mode 100644 index 0000000..d5bcfdf --- /dev/null +++ b/src/ConfigSerializer.cpp @@ -0,0 +1,175 @@ +/* + * MIT License + * + * Copyright (c) 2022 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ConfigSerializer.h" + + +using namespace config_serializer; +using namespace config_serializer::detail; +using namespace std; + +std::string config_serializer::detail::trim(const std::string &v) +{ + size_t start = v.find_first_not_of(' '); + size_t end = v.find_last_not_of(' '); + if (start >= end+1) + return ""; + return v.substr(start, end+1 - start); +} + +std::vector config_serializer::detail::split(const std::string &value, char delimiter) +{ + size_t start = 0; + std::vector result; + while (start < value.length()) + { + size_t pos = value.find_first_of(delimiter, start); + if (pos == std::string::npos) + { + result.push_back(value.substr(start)); + break; + } + result.push_back(value.substr(start, pos - start)); + start = pos + 1; + if (start == value.length()) + { + // ends with delimieter? Then there's an empty-length value at the end. + result.push_back(""); + } + } + return result; +} + + + +std::string config_serializer::detail::EncodeString(const std::string &s) +{ + bool requiresEncoding = false; + + for (char c : s) + { + switch (c) + { + case '\0': + break; + case '\r': + case '\t': + case '\\': + case ' ': + case '\"': + requiresEncoding = true; + break; + default: + break; + } + } + if (!requiresEncoding) + return s; + + std::stringstream ss; + + ss << '"'; + for (char c : s) + { + switch (c) + { + case '\0': + // discard. + break; + case '\r': + ss << "\\r"; + break; + case '\t': + ss << "\\t"; + break; + case '\\': + ss << "\\\\"; + break; + case '\"': + ss << "\\\""; + break; + default: + ss << c; + } + } + ss << '"'; + return ss.str(); +} + +std::string config_serializer::detail::DecodeString(const std::string &value) +{ + std::stringstream s; + const char *p = value.c_str(); + char quoteChar; + if (*p == '\'' || *p == '\"') + { + quoteChar = *p; + } + else + { + return value; + } + if (*p != quoteChar) + return value; + if (value.at(value.length()-1) != quoteChar) + { + throw std::invalid_argument("Invalid quoted string."); + } + ++p; + while (*p != 0 && *p != quoteChar) + { + char c = *p++; + if (c == '\\') + { + c = *p++; + switch (c) + { + case 'r': + s << '\r'; + break; + case 't': + s << '\t'; + break; + case 'n': + s << '\n'; + break; + case '\0': + throw std::invalid_argument("Invalid quoted string."); + break; + default: + s << c; + } + if (c != 0) + { + s << c; + } + } + else + { + s << c; + } + } + return s.str(); +} + diff --git a/src/ConfigSerializer.h b/src/ConfigSerializer.h new file mode 100644 index 0000000..0cd5dfa --- /dev/null +++ b/src/ConfigSerializer.h @@ -0,0 +1,369 @@ +/* + * MIT License + * + * Copyright (c) 2022 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include "autoptr_vector.h" +#include +#include + +namespace config_serializer +{ + namespace detail + { + std::string trim(const std::string &v); + + std::vector split(const std::string &v, char seperator); + + /** + * @brief Convert string to config file format. + * + * Adds quotes and escapes, but only if neccessary. + * + * Source text is assumed to be UTF-8. Escapes for the + * following values only: \r \n \t \" \\. + * + * + * @param s + * @return std::string Encoded string. + */ + std::string EncodeString(const std::string &s); + + /** + * @brief Config file format to string. + * + * Decodes quotes and escapes, but only if neccessary. + * + * Source text is assumed to be UTF-8. Escapes for the + * following values only: \r \n \t \" \\. + * + * @param s + * @return std::string + */ + std::string DecodeString(const std::string &s); + + /** + * @brief Convert a string to an integer. + * + * @tparam T A signed or unsigned integral type. + * @param value The string to parse. + * @return T The parsed result. + * @throws std::invalid_argument if the supplied string is not a valid integer. + */ + template + T ToInt(const std::string &value) + { + std::stringstream s(value); + T result; + s >> result; + if (s.fail()) + { + throw std::invalid_argument("Invalid integer value."); + } + return result; + } + + } + + using namespace detail; + + template + class ConfigSerializerBase + { + protected: + ConfigSerializerBase(const std::string &name, const std::string &comment) + : name(name), + comment(comment) + { + } + + public: + using Obj = OBJ; + virtual ~ConfigSerializerBase() {} + + const std::string &GetName() const { return name; } + const std::string &GetComment() const { return comment; } + virtual void SetValue(Obj &configurable, const std::string &value) const = 0; + virtual const std::string GetValue(Obj &configurable) const = 0; + + private: + std::string name; + std::string comment; + }; + + template + class ConfigConverter + { + public: + static T FromString(const std::string &value) + { + T result; + std::stringstream s(value); + s >> result; + return result; + } + + static std::string ToString(const T &value) + { + std::stringstream s; + s << value; + return s.str(); + } + }; + + // string specializations. + template <> + inline std::string ConfigConverter::FromString(const std::string &value) + { + return DecodeString(value); + } + template <> + inline std::string ConfigConverter::ToString(const std::string &config) + { + return EncodeString(config); + } + // bool specializations. + template <> + inline bool ConfigConverter::FromString(const std::string &value) + { + bool result = false; + if (value == "") + { + result = false; + } + else if (value == "true") + { + result = true; + } + else if (value == "false") + { + result = false; + } + else + { + try + { + int t = ToInt(value); + result = (t != 0); + } + catch (const std::exception &) + { + throw std::invalid_argument("Expecting true or false."); + } + } + return result; + } + template <> + inline std::string ConfigConverter::ToString(const bool &value) + { + return value ? "true" : "false"; + } + + template + class ConfigSerializer : public ConfigSerializerBase + { + public: + using Obj = OBJ; + using pointer_type = T Obj::*; + using base = ConfigSerializerBase; + using converter = ConfigConverter; + + ConfigSerializer(const std::string &name, + pointer_type pMember, + const std::string &comment) + : base(name, comment), + pMember(pMember) + { + } + virtual void SetValue(Obj &configurable, const std::string &value) const + { + configurable.*pMember = ConfigConverter::FromString(value); + } + virtual const std::string GetValue(Obj &configurable) const + { + return ConfigConverter::ToString(configurable.*pMember); + } + + private: + pointer_type pMember; + }; + + template + class ConfigSerializable + { + private: + using Obj = OBJ; + + using serializer_t = ConfigSerializerBase; + using serializers_t = p2p::autoptr_vector; + + const serializers_t &serializers; + + protected: + ConfigSerializable(const serializers_t &serializers) + : serializers(serializers) + { + } + + public: + virtual void Save(std::ostream &f) + { + bool firstLine = true; + for (const serializer_t *serializer : serializers) + { + if (serializer->GetComment().length() != 0) + { + if (!firstLine) + { + f << std::endl; + } + firstLine = false; + std::vector comments = split(serializer->GetComment(), '\n'); + + if (comments.size() != 0) + for (const std::string &comment : comments) + { + f << "# " << comment << std::endl; + } + } + + f << serializer->GetName() << '=' << serializer->GetValue((Obj &)*this) << std::endl; + } + } + void Save(const std::string &path) + { + std::ofstream f; + f.open(path); + if (!f.is_open()) + { + throw std::invalid_argument("Can't write to file " + path); + } + try + { + Save(f); + } + catch (const std::exception &e) + { + throw std::invalid_argument("Error writing to '" + path + "'. " + e.what()); + } + } + + virtual void Load(std::istream &f) + { + std::unordered_map index; + + for (serializer_t *serializer : serializers) + { + index[serializer->GetName()] = serializer; + } + + std::string line; + int nLine = 0; + while (true) + { + if (f.eof()) + break; + getline(f, line); + ++nLine; + + // remove comments. + auto npos = line.find_first_of('#'); + if (npos != std::string::npos) + { + line = line.substr(0, npos); + } + size_t start = 0; + while (start < line.size() && line[start] == ' ') + { + ++start; + } + if (start == line.size()) + { + continue; + } + + auto pos = line.find_first_of('=', start); + if (pos == std::string::npos) + { + std::stringstream msg; + + msg << "(" << nLine << "," << (start + 1) << "): Syntax error. Expecting '='."; + throw std::logic_error(msg.str()); + } + std::string label = trim(line.substr(start, pos - start)); + std::string value = trim(line.substr(pos + 1)); + + if (index.contains(label)) + { + serializer_t *serializer = index[label]; + try + { + serializer->SetValue((Obj &)*this, value); + } + catch (const std::exception &e) + { + std::stringstream msg; + msg << "(" << nLine << ',' << (pos + 1) << "): " << e.what(); + throw std::logic_error(msg.str()); + } + } + else + { + std::stringstream msg; + msg << "(" << nLine << ',' << (start) << "): " + << "Invalid property: " + label; + throw std::logic_error( + msg.str()); + } + } + } + + void Load(const std::string &path, bool throwIfNotFound = false) + { + try + { + std::ifstream f; + f.open(path); + if (!f.is_open()) + { + if (throwIfNotFound) + { + throw std::logic_error("Can't open file."); + } + return; + } + Load(f); + } + catch (const std::exception &e) + { + std::stringstream msg; + msg << "Error: " << path << " " << e.what(); + throw std::logic_error(msg.str()); + } + } + }; +} diff --git a/src/JackHost.cpp b/src/JackHost.cpp index b338a33..2e7254a 100644 --- a/src/JackHost.cpp +++ b/src/JackHost.cpp @@ -161,6 +161,12 @@ private: isOpen = false; StopReaderThread(); + + if (realtimeMonitorPortSubscriptions != nullptr) + { + delete realtimeMonitorPortSubscriptions; + realtimeMonitorPortSubscriptions = nullptr; + } if (active) { audioDriver->Deactivate(); @@ -933,8 +939,9 @@ public: Lv2Log::info("Audio started."); } - catch (PiPedalException &e) + catch (const std::exception &e) { + Lv2Log::error(SS("Failed to start audio. " << e.what())); Close(); active = false; throw; diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp index d53f3da..4ac7683 100644 --- a/src/Lv2Host.cpp +++ b/src/Lv2Host.cpp @@ -785,6 +785,9 @@ Lv2PortInfo::Lv2PortInfo(Lv2Host *host, const LilvPlugin *plugin, const LilvPort NodeAutoFree unitsValueUri = lilv_port_get(plugin, pPort, host->lilvUris.unitsUri); this->units_ = UriToUnits(nodeAsString(unitsValueUri)); + NodeAutoFree commentNode = lilv_port_get(plugin,pPort,host->lilvUris.rdfsComment); + this->comment_ = nodeAsString(commentNode); + NodeAutoFree bufferType = lilv_port_get(plugin, pPort, host->lilvUris.bufferType_uri); this->buffer_type_ = ""; @@ -1167,6 +1170,7 @@ json_map::storage_type Lv2PortInfo::jmap{{ MAP_REF(Lv2PortInfo, buffer_type), MAP_REF(Lv2PortInfo, port_group), json_map::enum_reference("units", &Lv2PortInfo::units_, get_units_enum_converter()), + MAP_REF(Lv2PortInfo, comment) }}; json_map::storage_type Lv2PortGroup::jmap{{ @@ -1229,6 +1233,7 @@ json_map::storage_type Lv2PluginUiControlPort::jmap{{ MAP_REF(Lv2PluginUiControlPort, port_group), json_map::enum_reference("units", &Lv2PluginUiControlPort::units_, get_units_enum_converter()), + MAP_REF(Lv2PluginUiControlPort, comment), }}; diff --git a/src/Lv2Host.hpp b/src/Lv2Host.hpp index d93e1a6..5d8197c 100644 --- a/src/Lv2Host.hpp +++ b/src/Lv2Host.hpp @@ -205,6 +205,7 @@ private: std::string designation_; Units units_ = Units::none; + std::string comment_; public: bool IsSwitch() const { return min_value_ == 0 && max_value_ == 1 @@ -261,6 +262,7 @@ public: LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(not_on_gui); LV2_PROPERTY_GETSET(port_group); + LV2_PROPERTY_GETSET(comment); LV2_PROPERTY_GETSET_SCALAR(units); LV2_PROPERTY_GETSET(buffer_type); @@ -417,6 +419,7 @@ public: , toggled_property_(pPort->toggled_property()) , not_on_gui_(pPort->not_on_gui()) , scale_points_(pPort->scale_points()) + , comment_(pPort->comment()) , units_(pPort->units()) { // Use symbols to index port groups, instead of uris. @@ -450,6 +453,7 @@ private: std::vector scale_points_; std::string port_group_; Units units_ = Units::none; + std::string comment_; public: LV2_PROPERTY_GETSET(symbol); LV2_PROPERTY_GETSET_SCALAR(index); @@ -466,6 +470,7 @@ public: LV2_PROPERTY_GETSET_SCALAR(not_on_gui); LV2_PROPERTY_GETSET(scale_points); LV2_PROPERTY_GETSET(units); + LV2_PROPERTY_GETSET(comment); public: static json_map::storage_type jmap; diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 83ec572..60a14fc 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -18,8 +18,9 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "pch.h" -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include "AudioConfig.hpp" +#include "ConfigUtil.hpp" #include #include "PiPedalModel.hpp" #include "JackHost.hpp" @@ -55,8 +56,6 @@ PiPedalModel::PiPedalModel() #else this->jackServerSettings = this->storage.GetJackServerSettings(); #endif - - } void PiPedalModel::Close() @@ -84,37 +83,40 @@ void PiPedalModel::Close() PiPedalModel::~PiPedalModel() { - try { - adminClient.UnmonitorGovernor(); - } catch (...) // noexcept here! + try + { + adminClient.UnmonitorGovernor(); + } + catch (...) // noexcept here! { - } - try { + try + { CurrentPreset currentPreset; currentPreset.modified_ = this->hasPresetChanged; currentPreset.preset_ = this->pedalBoard; storage.SaveCurrentPreset(currentPreset); - } catch (...) - { - } - - try { + catch (...) + { + } + + try + { if (jackHost) { jackHost->Close(); } - } catch (...) + } + catch (...) { - } } #include -void PiPedalModel::Init(const PiPedalConfiguration&configuration) +void PiPedalModel::Init(const PiPedalConfiguration &configuration) { this->configuration = configuration; storage.SetConfigRoot(configuration.GetDocRoot()); @@ -122,7 +124,6 @@ void PiPedalModel::Init(const PiPedalConfiguration&configuration) storage.Initialize(); this->jackServerSettings.ReadJackConfiguration(); - } void PiPedalModel::LoadLv2PluginInfo() @@ -147,14 +148,14 @@ void PiPedalModel::LoadLv2PluginInfo() // 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 : lv2Host.GetPlugins()) { if (plugin->has_factory_presets()) { if (!storage.HasPluginPresets(plugin->uri())) { PluginPresets pluginPresets = lv2Host.GetFactoryPluginPresets(plugin->uri()); - storage.SavePluginPresets(plugin->uri(),pluginPresets); + storage.SavePluginPresets(plugin->uri(), pluginPresets); } } } @@ -165,7 +166,6 @@ void PiPedalModel::Load() this->webRoot = configuration.GetWebRoot(); this->webPort = (uint16_t)configuration.GetSocketServerPort(); - adminClient.MonitorGovernor(storage.GetGovernorSettings()); // lv2Host.Load(configuration.GetLv2Path().c_str()); @@ -200,8 +200,8 @@ void PiPedalModel::Load() struct sched_param scheduler_params; scheduler_params.sched_priority = 10; - memset(&scheduler_params,0,sizeof(sched_param)); - sched_setscheduler(0,SCHED_RR,&scheduler_params); + memset(&scheduler_params, 0, sizeof(sched_param)); + sched_setscheduler(0, SCHED_RR, &scheduler_params); #if JACK_HOST this->jackConfiguration = this->jackConfiguration.JackInitialize(); #else @@ -216,7 +216,7 @@ void PiPedalModel::Load() this->lv2Host.OnConfigurationChanged(jackConfiguration, selection); try { - jackHost->Open(this->jackServerSettings,selection); + jackHost->Open(this->jackServerSettings, selection); std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; @@ -226,7 +226,9 @@ void PiPedalModel::Load() { Lv2Log::error("Failed to load initial plugin. (%s)", e.what()); } - } else { + } + else + { Lv2Log::error("Audio device not configured."); } } @@ -453,7 +455,7 @@ void PiPedalModel::FirePresetsChanged(int64_t clientId) delete[] t; } } -void PiPedalModel::FirePluginPresetsChanged(const std::string & pluginUri) +void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri) { std::lock_guard guard{mutex}; { @@ -473,7 +475,6 @@ void PiPedalModel::FirePluginPresetsChanged(const std::string & pluginUri) } } - void PiPedalModel::SaveCurrentPreset(int64_t clientId) { std::lock_guard guard{mutex}; @@ -482,31 +483,31 @@ void PiPedalModel::SaveCurrentPreset(int64_t clientId) this->SetPresetChanged(clientId, false); } -uint64_t PiPedalModel::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId) +uint64_t PiPedalModel::CopyPluginPreset(const std::string &pluginUri, uint64_t presetId) { - uint64_t result = storage.CopyPluginPreset(pluginUri,presetId); + uint64_t result = storage.CopyPluginPreset(pluginUri, presetId); FirePluginPresetsChanged(pluginUri); - return result; + return result; } -void PiPedalModel::UpdatePluginPresets(const PluginUiPresets&pluginPresets) +void PiPedalModel::UpdatePluginPresets(const PluginUiPresets &pluginPresets) { storage.UpdatePluginPresets(pluginPresets); FirePluginPresetsChanged(pluginPresets.pluginUri_); } -int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string&name) +int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string &name) { auto *item = this->pedalBoard.GetItem(instanceId); if (!item) { throw PiPedalException("Plugin not found."); } - std::map values; - for (const auto & controlValue: item->controlValues()) + std::map values; + for (const auto &controlValue : item->controlValues()) { values[controlValue.key()] = controlValue.value(); } - uint64_t presetId = storage.SavePluginPreset(item->uri(),name,values); + uint64_t presetId = storage.SavePluginPreset(item->uri(), name, values); FirePluginPresetsChanged(item->uri()); return presetId; } @@ -522,16 +523,15 @@ int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &n return result; } -void PiPedalModel::UploadPluginPresets(const PluginPresets&pluginPresets) +void PiPedalModel::UploadPluginPresets(const PluginPresets &pluginPresets) { if (pluginPresets.pluginUri_.length() == 0) { throw PiPedalException("Invalid plugin presets."); } std::lock_guard guard{mutex}; - storage.SavePluginPresets(pluginPresets.pluginUri_,pluginPresets); + storage.SavePluginPresets(pluginPresets.pluginUri_, pluginPresets); FirePluginPresetsChanged(pluginPresets.pluginUri_); - } int64_t PiPedalModel::UploadPreset(const BankFile &bankFile, int64_t uploadAfter) { @@ -623,7 +623,7 @@ int64_t PiPedalModel::DeletePreset(int64_t clientId, int64_t instanceId) -1, // can't use cached version. newSelection); } - return newSelection; + return newSelection; } bool PiPedalModel::RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name) { @@ -649,13 +649,13 @@ GovernorSettings PiPedalModel::GetGovernorSettings() return result; } } -void PiPedalModel::SetGovernorSettings(const std::string& governor) +void PiPedalModel::SetGovernorSettings(const std::string &governor) { std::lock_guard guard(mutex); adminClient.SetGovernorSettings(governor); this->storage.SetGovernorSettings(governor); - + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; { for (size_t i = 0; i < subscribers.size(); ++i) @@ -669,9 +669,8 @@ void PiPedalModel::SetGovernorSettings(const std::string& governor) } delete[] t; } - } - + void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings) { std::lock_guard guard(mutex); @@ -698,22 +697,46 @@ void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSet } } + +static std::string GetP2pdName() +{ + std::string name = "/etc/pipedal/config/pipedal_p2pd.conf"; + + std::string result; + + if (ConfigUtil::GetConfigLine(name,"p2p_device_name",&result)) + { + return result; + } + return ""; +} + void PiPedalModel::UpdateDnsSd() { avahiService.Unannounce(); - DeviceIdFile deviceIdFile; + ServiceConfiguration deviceIdFile; deviceIdFile.Load(); + + + std::string p2pdName = GetP2pdName(); + if (p2pdName != "") { + deviceIdFile.deviceName = p2pdName; + } + if (deviceIdFile.deviceName != "" && deviceIdFile.uuid != "") { - avahiService.Announce(webPort,deviceIdFile.deviceName,deviceIdFile.uuid,"pipedal"); - } else { + avahiService.Announce(webPort, deviceIdFile.deviceName, deviceIdFile.uuid, "pipedal"); + + + } + else + { // device_uuid file is written at install time. This warning is harmless if you're debugging. // Without it, we can't pulblish the website via dnsDS. // Run "pipedalconfig --install" to create the file. - Lv2Log::warning("Cant read device_uuid file. dnsSD announcement skipped."); + Lv2Log::warning("Cant read device_uuid file from service.conf file. dnsSD announcement skipped."); } - } void PiPedalModel::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings) { @@ -741,11 +764,7 @@ void PiPedalModel::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &w // update NSD-SD announement. UpdateDnsSd(); - - - } - } WifiConfigSettings PiPedalModel::GetWifiConfigSettings() @@ -786,7 +805,6 @@ bool PiPedalModel::GetShowStatusMonitor() { std::lock_guard guard(mutex); // copy atomically. return storage.GetShowStatusMonitor(); - } JackConfiguration PiPedalModel::GetJackConfiguration() @@ -795,37 +813,82 @@ JackConfiguration PiPedalModel::GetJackConfiguration() return this->jackConfiguration; } -void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) +void PiPedalModel::RestartAudio() { - std::lock_guard guard(mutex); // copy atomically. - this->storage.SetJackChannelSelection(channelSelection); - this->FireChannelSelectionChanged(clientId); - - this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); if (this->jackHost->IsOpen()) { this->jackHost->Close(); -#ifdef JUNK - // restarting is a bit dodgy. It was impossible with Jack, but - // now very plausible with the ALSA audio stack. - + } + // restarting is a bit dodgy. It was impossible with Jack, but + // now very plausible with the ALSA audio stack. + // Still bugs wrt/ restarting the circular buffers for the audio thread. - // for the meantime, just rely on the fact that the admin service will restart + // for the meantime, just rely on the fact that the admin service will restart // the process. //... // do a complete reload. - this->jackHost->SetPedalBoard(nullptr); - this->jackHost->Open(this->jackServerSettings,channelSelection); + this->jackHost->SetPedalBoard(nullptr); + + this->jackConfiguration.AlsaInitialize(this->jackServerSettings); + + if (this->jackConfiguration.isValid()) + { + JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); + selection = selection.RemoveInvalidChannels(jackConfiguration); + + } else { + jackConfiguration.SetErrorStatus("Error"); + } + + FireJackConfigurationChanged(jackConfiguration); + + + if (!jackServerSettings.IsValid() || !jackConfiguration.isValid()) + { + Lv2Log::error("Audio configuration not valid."); + return; + } + try + { + JackChannelSelection channelSelection = GetJackChannelSelection(); + if (!channelSelection.isValid()) + { + Lv2Log::error("Audio configuration not valid."); + return; + } + this->jackHost->Open(this->jackServerSettings, channelSelection); + + this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); + std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; jackHost->SetPedalBoard(lv2PedalBoard); this->UpdateRealtimeVuSubscriptions(); -#endif + UpdateRealtimeMonitorPortSubscriptions(); } + catch (const std::exception &e) + { + Lv2Log::error(SS("Failed to start audio. " << e.what())); + throw; + } +} + +void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) +{ + { + std::lock_guard guard(mutex); // copy atomically. + this->storage.SetJackChannelSelection(channelSelection); + + this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); + } + + RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. + + this->FireChannelSelectionChanged(clientId); } void PiPedalModel::FireChannelSelectionChanged(int64_t clientId) @@ -1024,7 +1087,6 @@ void PiPedalModel::UnmonitorPort(int64_t subscriptionHandle) } } - void PiPedalModel::OnNotifyMonitorPort(const MonitorPortUpdate &update) { std::lock_guard guard(mutex); @@ -1095,7 +1157,7 @@ void PiPedalModel::GetLv2Parameter( BankIndex PiPedalModel::GetBankIndex() const { - std::lock_guard guard(const_cast(mutex)); + std::lock_guard guard(const_cast(mutex)); return storage.GetBanks(); } @@ -1137,16 +1199,17 @@ JackServerSettings PiPedalModel::GetJackServerSettings() void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSettings) { - std::lock_guard guard(mutex); + std::unique_lock guard(mutex); +#if JACK_HOST if (!adminClient.CanUseShutdownClient()) { throw PiPedalException("Can't change server settings when running a debug server."); } +#endif this->jackServerSettings = jackServerSettings; - // 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) @@ -1160,19 +1223,25 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet } delete[] t; + +#if ALSA_HOST + storage.SetJackServerSettings(jackServerSettings); + + FireJackConfigurationChanged(this->jackConfiguration); + + guard.unlock(); + RestartAudio(); + +#endif +#if JACK_HOST if (adminClient.CanUseShutdownClient()) { - - #if ALSA_HOST - storage.SetJackServerSettings(jackServerSettings); - #endif - // save the current (edited) preset now in case the service shutdown isn't clean. CurrentPreset currentPreset; currentPreset.modified_ = this->hasPresetChanged; currentPreset.preset_ = this->pedalBoard; storage.SaveCurrentPreset(currentPreset); - + this->jackConfiguration.SetIsRestarting(true); FireJackConfigurationChanged(this->jackConfiguration); this->jackHost->UpdateServerConfiguration( @@ -1195,8 +1264,8 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet } else { - // we now do a complete restart of the services, - // so just sit tight and wait for the restart. + // we now do a complete restart of the services, + // so just sit tight and wait for the restart. #ifdef JUNK this->jackConfiguration.SetErrorStatus(""); FireJackConfigurationChanged(this->jackConfiguration); @@ -1211,7 +1280,9 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet #endif } }); - } + + } +#endif } void PiPedalModel::UpdateDefaults(PedalBoardItem *pedalBoardItem) @@ -1259,15 +1330,14 @@ void PiPedalModel::UpdateDefaults(PedalBoard *pedalBoard) } } -PluginPresets PiPedalModel::GetPluginPresets(const std::string& pluginUri) +PluginPresets PiPedalModel::GetPluginPresets(const std::string &pluginUri) { std::lock_guard guard(mutex); return storage.GetPluginPresets(pluginUri); - } -PluginUiPresets PiPedalModel::GetPluginUiPresets(const std::string& pluginUri) +PluginUiPresets PiPedalModel::GetPluginUiPresets(const std::string &pluginUri) { std::lock_guard guard(mutex); @@ -1332,7 +1402,7 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) jackHost->SetListenForMidiEvent(midiEventListeners.size() != 0); } -void PiPedalModel::OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson) +void PiPedalModel::OnNotifyAtomOutput(uint64_t instanceId, const std::string &atomType, const std::string &atomJson) { std::lock_guard guard(mutex); @@ -1344,19 +1414,18 @@ void PiPedalModel::OnNotifyAtomOutput(uint64_t instanceId, const std::string&ato auto subscriber = this->GetNotificationSubscriber(listener.clientId); if (subscriber) { - subscriber->OnNotifyAtomOutput(listener.clientHandle,instanceId,atomType,atomJson); - } else { + subscriber->OnNotifyAtomOutput(listener.clientHandle, instanceId, atomType, atomJson); + } + else + { atomOutputListeners.erase(atomOutputListeners.begin() + i); --i; } } } jackHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); - } - - void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) { std::lock_guard guard(mutex); @@ -1370,7 +1439,9 @@ void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) if (subscriber) { subscriber->OnNotifyMidiListener(listener.clientHandle, isNote, noteOrControl); - } else { + } + else + { midiEventListeners.erase(midiEventListeners.begin() + i); --i; } @@ -1400,7 +1471,7 @@ void PiPedalModel::CancelListenForMidiEvent(int64_t clientId, int64_t clientHand std::lock_guard guard(mutex); for (size_t i = 0; i < midiEventListeners.size(); ++i) { - const auto& listener = midiEventListeners[i]; + const auto &listener = midiEventListeners[i]; if (listener.clientId == clientId && listener.clientHandle == clientHandle) { midiEventListeners.erase(midiEventListeners.begin() + i); @@ -1417,7 +1488,7 @@ void PiPedalModel::CancelListenForAtomOutputs(int64_t clientId, int64_t clientHa std::lock_guard guard(mutex); for (size_t i = 0; i < atomOutputListeners.size(); ++i) { - const auto&listener = atomOutputListeners[i]; + const auto &listener = atomOutputListeners[i]; if (listener.clientId == clientId && listener.clientHandle == clientHandle) { atomOutputListeners.erase(atomOutputListeners.begin() + i); @@ -1434,24 +1505,24 @@ std::vector PiPedalModel::GetAlsaDevices() return this->alsaDevices.GetAlsaDevices(); } -const std::filesystem::path& PiPedalModel::GetWebRoot() const +const std::filesystem::path &PiPedalModel::GetWebRoot() const { return webRoot; } -std::map PiPedalModel::GetFavorites() const +std::map PiPedalModel::GetFavorites() const { - std::lock_guard guard(const_cast(mutex)); - + std::lock_guard guard(const_cast(mutex)); + return storage.GetFavorites(); } -void PiPedalModel::SetFavorites(const std::map &favorites) +void PiPedalModel::SetFavorites(const std::map &favorites) { std::lock_guard guard(mutex); storage.SetFavorites(favorites); // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t; + std::vector t; t.reserve(this->subscribers.size()); for (size_t i = 0; i < subscribers.size(); ++i) diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 5a93006..dd2a7cc 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -142,6 +142,8 @@ private: void UpdateRealtimeMonitorPortSubscriptions(); void OnVuUpdate(const std::vector& updates); + void RestartAudio(); + std::vector outstandingParameterRequests; IPiPedalModelSubscriber*GetNotificationSubscriber(int64_t clientId); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 91b0095..bc4bf46 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -836,10 +836,10 @@ public: std::string governor; pReader->read(&governor); std::string fromAddress = this->getFromAddress(); - if (!IsOnLocalSubnet(fromAddress)) - { - throw PiPedalException("Permission denied. Not on local subnet."); - } + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } this->model.SetGovernorSettings(governor); this->Reply(replyTo, "setGovernorSettings"); } @@ -852,10 +852,10 @@ public: throw PiPedalException("Can't change server settings when running interactively."); } std::string fromAddress = this->getFromAddress(); - if (!IsOnLocalSubnet(fromAddress)) - { - throw PiPedalException("Permission denied. Not on local subnet."); - } + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } this->model.SetWifiConfigSettings(wifiConfigSettings); this->Reply(replyTo, "setWifiConfigSettings"); @@ -873,10 +873,10 @@ public: throw PiPedalException("Can't change server settings when running interactively."); } std::string fromAddress = this->getFromAddress(); - if (!IsOnLocalSubnet(fromAddress)) - { - throw PiPedalException("Permission denied. Not on local subnet."); - } + // if (!IsOnLocalSubnet(fromAddress)) + // { + // throw PiPedalException("Permission denied. Not on local subnet."); + // } this->model.SetWifiDirectConfigSettings(wifiDirectConfigSettings); this->Reply(replyTo, "setWifiDirectConfigSettings"); diff --git a/src/DeviceIdFile.cpp b/src/ServiceConfiguration.cpp similarity index 62% rename from src/DeviceIdFile.cpp rename to src/ServiceConfiguration.cpp index 027f0cc..ae8abb7 100644 --- a/src/DeviceIdFile.cpp +++ b/src/ServiceConfiguration.cpp @@ -22,7 +22,7 @@ * SOFTWARE. */ -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include #include #include @@ -33,44 +33,45 @@ using namespace pipedal; using namespace std; +using namespace config_serializer; -const char DeviceIdFile::DEVICEID_FILE_NAME[] = "/etc/pipedal/config/device_uuid"; +const char ServiceConfiguration::DEVICEID_FILE_NAME[] = "/etc/pipedal/config/service.conf"; -void DeviceIdFile::Load() +#define SERIALIZER_ENTRY(MEMBER_NAME) \ + new ConfigSerializer(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "") + + +static p2p::autoptr_vector> +deviceIdSerializers +{ + SERIALIZER_ENTRY(uuid), + SERIALIZER_ENTRY(deviceName), + SERIALIZER_ENTRY(server_port) +}; + + +ServiceConfiguration::ServiceConfiguration() +: base(deviceIdSerializers) { - { - ifstream f; - f.open(DEVICEID_FILE_NAME); - if (f.is_open()) - { - std::getline(f, uuid); - std::getline(f, deviceName); - } - } } -void DeviceIdFile::Save() -{ - { - std::string path = DEVICEID_FILE_NAME; - ofstream f; - f.open(path, ios_base::trunc); - if (!f.is_open()) - { - throw invalid_argument("Can't write to file " + path); - } +void ServiceConfiguration::Load() +{ + base::Load(DEVICEID_FILE_NAME,false); +} +void ServiceConfiguration::Save() +{ + base::Save(DEVICEID_FILE_NAME); + { struct group *group_; group_ = getgrnam("pipedal_d"); if (group_ == nullptr) { throw logic_error("Group not found: pipedal_d"); } - std::ignore = chown(path.c_str(), -1, group_->gr_gid); - std::ignore = chmod(path.c_str(), S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); - - f << uuid << endl; - f << deviceName << endl; + std::ignore = chown(DEVICEID_FILE_NAME,-1,group_->gr_gid); + std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); } } \ No newline at end of file diff --git a/src/DeviceIdFile.hpp b/src/ServiceConfiguration.hpp similarity index 80% rename from src/DeviceIdFile.hpp rename to src/ServiceConfiguration.hpp index 073b961..eb70fbb 100644 --- a/src/DeviceIdFile.hpp +++ b/src/ServiceConfiguration.hpp @@ -25,10 +25,17 @@ #pragma once #include +#include "ConfigSerializer.h" namespace pipedal { - class DeviceIdFile { + using namespace config_serializer; + + class ServiceConfiguration: protected ConfigSerializable { public: + using base = ConfigSerializable; + + ServiceConfiguration(); + static const char DEVICEID_FILE_NAME[]; void Load(); @@ -36,6 +43,7 @@ namespace pipedal { std::string uuid; - std::string deviceName; + std::string deviceName = "PiPedal"; + uint32_t server_port = 80; }; } \ No newline at end of file diff --git a/src/SetWifiConfig.cpp b/src/SetWifiConfig.cpp index 0a7902f..409fd03 100644 --- a/src/SetWifiConfig.cpp +++ b/src/SetWifiConfig.cpp @@ -21,7 +21,7 @@ #include "ss.hpp" #include "P2pConfigFiles.hpp" -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include "SetWifiConfig.hpp" #include "PiPedalException.hpp" #include "SystemConfigFile.hpp" @@ -410,6 +410,16 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) { cout << "Disabling P2P" << endl; UninstallP2p(); + + ServiceConfiguration deviceIdFile; + deviceIdFile.Load(); + deviceIdFile.deviceName = settings.hotspotName_; + deviceIdFile.Save(); + + + // Announce new mDNS service with potentially new deviceName. + sysExec(SYSTEMCTL_BIN " restart pipedald"); + } else { @@ -417,7 +427,7 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) settings.Save(); // ******************* device_uuid - DeviceIdFile deviceIdFile; + ServiceConfiguration deviceIdFile; deviceIdFile.Load(); deviceIdFile.deviceName = settings.hotspotName_; deviceIdFile.Save(); @@ -502,12 +512,9 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) sysExec(SYSTEMCTL_BIN " daemon-reload"); sysExec(SYSTEMCTL_BIN " stop pipedal_p2pd"); - sysExec(SYSTEMCTL_BIN " stop dnsmasq"); - sysExec(SYSTEMCTL_BIN " stop wpa_supplicant"); sysExec(SYSTEMCTL_BIN " restart dhcpcd"); - sysExec(SYSTEMCTL_BIN " unmask wpa_supplicant"); sysExec(SYSTEMCTL_BIN " start wpa_supplicant"); - sysExec(SYSTEMCTL_BIN " enable wpa_supplicant"); + sysExec(SYSTEMCTL_BIN " enable wpa_supplicant"); // ?? Not sure that's still right. :-/ sysExec(SYSTEMCTL_BIN " start dnsmasq"); sysExec(SYSTEMCTL_BIN " enable dnsmasq"); sysExec(SYSTEMCTL_BIN " start pipedal_p2pd"); diff --git a/src/WifiDirectConfigSettings.cpp b/src/WifiDirectConfigSettings.cpp index 97273b8..b24d590 100644 --- a/src/WifiDirectConfigSettings.cpp +++ b/src/WifiDirectConfigSettings.cpp @@ -25,7 +25,7 @@ #include #include #include "ConfigUtil.hpp" -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include "WriteTemplateFile.hpp" #include "ss.hpp" @@ -169,7 +169,7 @@ void WifiDirectConfigSettings::Load() this->channel_ = "6"; } try { - DeviceIdFile deviceIdFile; + ServiceConfiguration deviceIdFile; deviceIdFile.Load(); if (deviceIdFile.deviceName == "") { diff --git a/src/autoptr_vector.h b/src/autoptr_vector.h new file mode 100644 index 0000000..fff6224 --- /dev/null +++ b/src/autoptr_vector.h @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2022 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once +#include + + + +namespace p2p { + + template class autoptr_vector + { + std::vector v; + private: + // no copy no move. + autoptr_vector(const autoptr_vector &) = delete; + autoptr_vector(autoptr_vector && ) = delete; + public: + using iterator = std::vector::const_iterator; + + autoptr_vector() { } + autoptr_vector(std::initializer_list l) + :v(l) + { + + } + ~autoptr_vector() { + for (T *i: v) + { + delete i; + } + } + void push_back(T*value) { v.push_back(value);} + + iterator begin() const { return v.begin(); } + iterator end() const { return v.end(); } + + T*operator[](size_t index) const { return v[index]; } + + size_t size() const { return v.size(); } + }; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 8550920..8805600 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,7 @@ #include "BeastServer.hpp" #include #include "Lv2Log.hpp" -#include "DeviceIdFile.hpp" +#include "ServiceConfiguration.hpp" #include "AvahiService.hpp" #include "PiPedalSocket.hpp" @@ -428,7 +428,7 @@ public: #define LINK_LOCAL_WEB_SOCKET 1 #if LINK_LOCAL_WEB_SOCKET std::string webSocketAddress = GetLinkLocalAddress(fromAddress); - Lv2Log::info(SS("Web Socket Adddress: " << webSocketAddress << ":" << portNumber)); + Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber)); #else std::string webSocketAddress = "*"; #endif diff --git a/src/templates/pipedal_p2pd.service.template b/src/templates/pipedal_p2pd.service.template index 9de872c..67e31e1 100644 --- a/src/templates/pipedal_p2pd.service.template +++ b/src/templates/pipedal_p2pd.service.template @@ -1,14 +1,19 @@ [Unit] Description=PiPedal P2P Session Manager After=network.target +After=nss-lookup.target After=wpa_supplicant.service After=dhcpcd.service +StartLimitIntervalSec=300 +StartLimitBurst=8 [Service] ExecStart=${COMMAND} --systemd --log-level debug Type=notify WorkingDirectory=/var/pipedal TimeoutStopSec=5 +Restart=on-failure +RestartSec=10s [Install] WantedBy=multi-user.target diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template index 6d4af18..3fe571a 100644 --- a/src/templates/pipedald.service.template +++ b/src/templates/pipedald.service.template @@ -7,6 +7,7 @@ After=local-fs.target [Service] +Restart=on-failure Type=notify LimitMEMLOCK=infinity LimitRTPRIO=95 diff --git a/submodules/pipedal_p2pd b/submodules/pipedal_p2pd index 85b68b8..81111f1 160000 --- a/submodules/pipedal_p2pd +++ b/submodules/pipedal_p2pd @@ -1 +1 @@ -Subproject commit 85b68b8400608284062cbbba77cbcc377120ccbf +Subproject commit 81111f111fba3249f9a3853877bcd098e3c98250