diff --git a/src/jsonapi/jsonapi.cpp b/src/jsonapi/jsonapi.cpp index 2d91ee36f..2a1abf7f4 100644 --- a/src/jsonapi/jsonapi.cpp +++ b/src/jsonapi/jsonapi.cpp @@ -155,6 +155,7 @@ bool RsJsonApi::parseToken( } JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"), + mResourceProvidersMutex("JsonApiServer resource providers"), mService(nullptr), mListeningPort(RsJsonApi::DEFAULT_PORT), mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS), @@ -836,7 +837,10 @@ void JsonApiServer::handleCorsOptions( void JsonApiServer::registerResourceProvider(const JsonApiResourceProvider& rp) { - mResourceProviders.insert(rp); + { + RS_STACK_MUTEX(mResourceProvidersMutex); + mResourceProviders.insert(rp); + } if(rsEvents) { @@ -847,7 +851,10 @@ void JsonApiServer::registerResourceProvider(const JsonApiResourceProvider& rp) } void JsonApiServer::unregisterResourceProvider(const JsonApiResourceProvider& rp) { - mResourceProviders.erase(rp); + { + RS_STACK_MUTEX(mResourceProvidersMutex); + mResourceProviders.erase(rp); + } if(rsEvents) { @@ -857,10 +864,14 @@ void JsonApiServer::unregisterResourceProvider(const JsonApiResourceProvider& rp } } bool JsonApiServer::hasResourceProvider(const JsonApiResourceProvider& rp) -{ return mResourceProviders.find(rp) != mResourceProviders.end(); } - -const std::set,std::less >& JsonApiServer::getResourceProviders() const { + RS_STACK_MUTEX(mResourceProvidersMutex); + return mResourceProviders.find(rp) != mResourceProviders.end(); +} + +RsJsonApi::ResourceProviderSet JsonApiServer::getResourceProviders() const +{ + RS_STACK_MUTEX(mResourceProvidersMutex); return mResourceProviders; } @@ -868,7 +879,7 @@ std::vector > JsonApiServer::getResources() const { auto tab = mResources; - for(auto& rp: mResourceProviders) + for(auto& rp: getResourceProviders()) for(auto r: rp.get().getResources()) tab.push_back(r); return tab; diff --git a/src/jsonapi/jsonapi.h b/src/jsonapi/jsonapi.h index d4bd90bbc..48a66bd39 100644 --- a/src/jsonapi/jsonapi.h +++ b/src/jsonapi/jsonapi.h @@ -64,8 +64,8 @@ public: std::vector> getResources() const; - /// @see RsJsonApi - const std::set,std::less >& getResourceProviders() const override; + /// @see RsJsonApi + ResourceProviderSet getResourceProviders() const override; /// @see RsJsonApi void fullstop() override { RsThread::fullstop(); } @@ -180,6 +180,7 @@ private: /// Encrypted persistent storage for authorized JSON API tokens JsonApiServerAuthTokenStorage mAuthTokenStorage; RsMutex configMutex; + mutable RsMutex mResourceProvidersMutex; static const std::multimap corsHeaders; static const std::multimap corsOptionsHeaders; diff --git a/src/plugins/pluginmanager.cc b/src/plugins/pluginmanager.cc index 70371360e..99b333c7c 100644 --- a/src/plugins/pluginmanager.cc +++ b/src/plugins/pluginmanager.cc @@ -371,7 +371,17 @@ bool RsPluginManager::loadPlugin(const std::string& plugin_name,bool first_time) pinfo.info_string = "" ; dlclose(handle); return false ; - } + } + if(pinfo.API_version != RS_PLUGIN_API_VERSION) + { + std::cerr << " -> Incompatible plugin API version " + << std::hex << pinfo.API_version << "; expected " + << RS_PLUGIN_API_VERSION << std::dec << std::endl; + pinfo.status = PLUGIN_STATUS_WRONG_API; + pinfo.info_string = "Incompatible plugin API version"; + dlclose(handle); + return false; + } #ifdef TO_REMOVE if(pinfo.svn_revision == 0) { diff --git a/src/retroshare/rsjsonapi.h b/src/retroshare/rsjsonapi.h index 5f9ad4d87..de1121264 100644 --- a/src/retroshare/rsjsonapi.h +++ b/src/retroshare/rsjsonapi.h @@ -37,6 +37,11 @@ class RsJsonApi; /** * Pointer to global instance of RsJsonApi service implementation * @jsonapi{development} + * + * Beware that non-null does not imply configured: RsServer::StartupRetroShare() + * creates the server early so plugins can register resource providers, while + * the config manager, the authorized tokens, the listening port and the binding + * address are only applied later by RsInit::startupWebServices(). */ extern RsJsonApi* rsJsonApi; @@ -203,15 +208,21 @@ public: */ virtual void connectToConfigManager(p3ConfigMgr& cfgmgr) = 0; + using ResourceProviderSet = std::set< + std::reference_wrapper, + std::less >; + /** * This is used to add/remove new web services to JsonAPI. The client * should take care of not using a path range already used by the jsonAPI - * server + * server. + * @see getResourceProviders() returns a snapshot by value, so it stays + * valid while another thread registers or unregisters a provider. */ virtual void registerResourceProvider(const JsonApiResourceProvider&) = 0; virtual void unregisterResourceProvider(const JsonApiResourceProvider&) = 0; virtual bool hasResourceProvider(const JsonApiResourceProvider&) = 0; - virtual const std::set,std::less >& getResourceProviders() const =0; + virtual ResourceProviderSet getResourceProviders() const = 0; /** * @brief This function should be used by JSON API clients that aren't diff --git a/src/retroshare/rsplugin.h b/src/retroshare/rsplugin.h index f7cec2b52..708c38569 100644 --- a/src/retroshare/rsplugin.h +++ b/src/retroshare/rsplugin.h @@ -38,6 +38,7 @@ extern RsPluginHandler *rsPlugins ; class p3Service ; class RsServiceControl ; class RsReputations ; +class RsJsonApi; class RsTurtle ; class RsGxsTunnelService ; class RsDht ; @@ -87,10 +88,10 @@ namespace resource_api class StateTokenServer; } -// Plugin API version. Not used yet, but will be in the future the -// main value that decides for compatibility. +// Plugin API version. RsPluginManager rejects plugins built against a +// different version before calling into their ABI. // -#define RS_PLUGIN_API_VERSION 0x000101 +#define RS_PLUGIN_API_VERSION 0x000103 // Used for the status of plugins. // @@ -141,6 +142,9 @@ public: #ifdef RS_USE_WIRE RsUtil::inited_ptr mWire; #endif + + // Keep new members appended so offsets used by older plugins do not move. + RsUtil::inited_ptr mJsonApi; }; class RsPlugin diff --git a/src/rsserver/rsinit.cc b/src/rsserver/rsinit.cc index 31d6e08c4..dd0cdddb3 100644 --- a/src/rsserver/rsinit.cc +++ b/src/rsserver/rsinit.cc @@ -445,7 +445,24 @@ void RsInit::startupWebServices(const RsConfigOptions& conf,bool force_start_jso std::cerr << std::endl; RsInfo() << "Configuring web services" ; - JsonApiServer* jas = new JsonApiServer(); + // Plugins receive RsPlugInInterfaces during core startup, before the GUI + // calls startupWebServices(). Reuse the server created for that handoff so + // resource providers registered by plugins are not lost here. + JsonApiServer* jas = dynamic_cast(rsJsonApi); + if(!jas) + { + /* Not reachable today -- rsJsonApi is only ever set to a JsonApiServer, + * here and in the plugin handoff of StartupRetroShare(). Should that + * change, replacing it silently would leave every plugin holding the + * pointer it received in setInterfaces() talking to an abandoned + * object, with no trace of why. */ + if(rsJsonApi) + RsErr() << "rsJsonApi is set but is not a JsonApiServer. Replacing " + << "it: plugins still hold the previous pointer."; + + jas = new JsonApiServer(); + rsJsonApi = jas; + } bool jsonapi_needed = force_start_jsonapi; // add jsonapi server to config manager so that it can save/load its tokens @@ -529,8 +546,6 @@ void RsInit::startupWebServices(const RsConfigOptions& conf,bool force_start_jso } else RsInfo() << " Not starting JSON API, since it is currently not required by any service." ; - - rsJsonApi = jas; } #endif @@ -1675,6 +1690,22 @@ int RsServer::StartupRetroShare() #endif interfaces.mServiceControl = serviceCtrl; interfaces.mPluginHandler = mPluginsManager; +#ifdef RS_JSONAPI + // The GUI configures web services after plugins are initialized. Create + // the server now so plugins can register providers in setInterfaces(). + // startupWebServices() will configure and reuse this same instance later. + // + // This has to stay *after* the "if (rsJsonApi) connectToConfigManager()" + // block above: that block exists for retroshare-service and Android, where + // startupWebServices() already ran before login. Creating the server before + // it would make the GUI connect the config manager here and again in + // startupWebServices(), reloading jsonapi.cfg twice. + if(!rsJsonApi) + rsJsonApi = new JsonApiServer(); + interfaces.mJsonApi = rsJsonApi; +#else + interfaces.mJsonApi = nullptr; +#endif // gxs interfaces.mGxsDir = currGxsDir; interfaces.mIdentity = mGxsIdService; @@ -1690,8 +1721,42 @@ int RsServer::StartupRetroShare() interfaces.mGxsTunnels = mGxsTunnels; interfaces.mReputations = mReputations; interfaces.mPosted = mPosted; - + +#ifdef RS_JSONAPI + // Service and Android start JSON API before plugins are initialized. Take + // one snapshot here so all newly registered providers are published with + // at most one core-owned restart after every plugin received interfaces. + const bool jsonApiWasRunning = rsJsonApi && rsJsonApi->isRunning(); + const std::size_t jsonApiProviderCount = jsonApiWasRunning ? + rsJsonApi->getResourceProviders().size() : 0; +#endif mPluginsManager->setInterfaces(interfaces); +#ifdef RS_JSONAPI + if( jsonApiWasRunning && jsonApiProviderCount != + rsJsonApi->getResourceProviders().size() ) + { + RsInfo() << "Restarting JSON API once to publish plugin resources."; + + /* The restart must not run on the JSON API server thread. + * StartupRetroShare() is reached from RsLoginHelper::attemptLogin() and + * createLocationV2(), both exposed through the JSON API, and restbed + * serves its handlers on the thread that called Service::start(), which + * is JsonApiServer::run() itself. restart() calls RsThread::fullstop(), + * and joining our own thread makes waitWhileStopping() bail out with an + * error instead of waiting; the subsequent RsThread::start() then fails + * with "attempt to start already running thread" and the server stays + * down for good. The /rsJsonApi/restart handler takes the same care. + * Dispatching also keeps the RESTART_BURST_PROTECTION wait, which is + * meant to throttle API clients, off the startup path. */ + RsThread::async([]() + { + const std::error_condition restartError = rsJsonApi->restart(true); + if(restartError) + RsErr() << "Failed restarting JSON API after plugin " + << "initialization: " << restartError.message(); + }); + } +#endif // now add plugin objects inside the loop: // - client services provided by plugins.