Skip to content

Changes for JSONAPI to work with plugins - #364

Open
defnax wants to merge 10 commits into
RetroShare:masterfrom
defnax:api-for-plugins
Open

Changes for JSONAPI to work with plugins#364
defnax wants to merge 10 commits into
RetroShare:masterfrom
defnax:api-for-plugins

Conversation

@defnax

@defnax defnax commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

it works now with Feedreader plugin but im not sure if it breaks something needs to be reviewed
@G10h4ck @thunder2 @csoler

@jolavillette

Copy link
Copy Markdown
Contributor

364, 3289 and 124 add new entries in the reviewers todo list, which is already huge.
I don't see the point of adding new features, while there are still many bugs to fix every where.

Anyway here is an audit by Claude, to facilitate reviewing.

Reviewed at 894756b65, base master. Line numbers refer to the final state of the files in this PR. Judged together with RetroShare/RetroShare#3289, since this PR alone changes nothing observable — it only adds a member nobody reads yet.

The gap is real and worth closing: RsPlugInInterfaces never carried the JSON API, so a plugin had no supported way to publish routes. The shape chosen here — create the server earlier in StartupRetroShare(), hand the pointer to plugins, reuse the same instance in startupWebServices() — is the right idea, and the startup ordering was worked out correctly. In the GUI, StartupRetroShare() (retroshare-gui/src/main.cpp:640) runs before startupWebServices() (main.cpp:770). In retroshare-service, startupWebServices() (retroshare-service.cc:291) runs before login, hence before StartupRetroShare(), which is reached through attemptLogin() (retroshare-service.cc:361) → rsinit.cc:2230; the if(!rsJsonApi) guard reuses the existing instance correctly. Same for Android (src/rs_android/retroshareserviceandroid.cpp:78).

Three things need fixing before merge. The second changes behaviour for every user who has a provider-registering plugin installed, with no action on their part.


Blockers

1. mJsonApi is inserted in the middle of RsPlugInInterfaces, and the API version bump enforces nothing

src/retroshare/rsplugin.h:126 places the new member between mPluginHandler and the // gxs block, so mGxsDir, mIdentity, mRsNxsNetMgr, mGxsIdService, mGxsCirlces, mPgpAuxUtils, mGxsForums, mGxsChannels, mGxsTunnels, mReputations and mPosted all shift by one pointer.

Plugins do not read the struct in place, they copy it — plugins/FeedReader/FeedReaderPlugin.cpp, setInterfaces(): mInterfaces = interfaces;. That copy-assignment is compiled into the plugin, against the plugin's own layout. A .so built before this change therefore copies mGxsDir (a std::string) from the offset now holding mJsonApi, i.e. from a pointer value followed by the first bytes of the real string: undefined behaviour, in practice an immediate crash on plugin load.

RetroShare does have a guard here, and it is not the API version. RsPluginManager::loadList() (src/plugins/pluginmanager.cc:544) discards the accepted-plugin list when REFERENCE_EXECUTABLE_HASH no longer matches the hash of the running executable, so after an upgrade the user is re-prompted (NEW_PLUGIN_FOUND, pluginmanager.cc:317) for every plugin. But that is a consent gate, not a compatibility gate: a user who recognises "FeedReader" in ~/.retroshare/extensions6/ and clicks accept still loads the stale binary. And ALLOW_ALL_PLUGINS (pluginmanager.cc:513, exposed as the "enable all" checkbox in retroshare-gui/src/gui/settings/PluginsPage.cpp:175) skips the hash check entirely, in which case the stale .so is loaded with no prompt at all.

Meanwhile the constant that exists precisely for this does nothing. Plugins export it as RETROSHARE_PLUGIN_api (FeedReaderPlugin.cpp:67), the core reads it in loadPlugin() (pluginmanager.cc:360) into pinfo.API_version, and the only test is if(pinfo.API_version == 0) (pluginmanager.cc:367) — "is the symbol missing". The value is never compared with the core's own RS_PLUGIN_API_VERSION. The comment at rsplugin.h:91 says as much: "Not used yet".

Two fixes, both cheap:

  • Move mJsonApi to the end of the struct, after mPosted / mWire (rsplugin.h:144). Old plugins then keep every offset they know and simply never read the new member. One line, and the hazard is gone.
  • If the bump is meant to mean something, make loadPlugin() actually reject pinfo.API_version != RS_PLUGIN_API_VERSION, with a new PLUGIN_STATUS_INCOMPATIBLE_API next to the existing PLUGIN_STATUS_MISSING_API. Bumping a constant that nothing reads gives a false sense of safety.

In passing: the struct layout already varies with RS_USE_CALENDAR and RS_USE_WIRE, so the project implicitly assumes plugins are rebuilt from the same tree. That is an argument for appending at the end and enforcing the version, not for ignoring the problem.

2. jsonapi_needed starts a listening HTTP server the user never asked for

src/rsserver/rsinit.cc:459:

bool jsonapi_needed = force_start_jsonapi || !jas->getResourceProviders().empty();

In the GUI, force_start_jsonapi is false (main.cpp:770). On master the JSON API is started at boot only when the Web UI is enabled with a password. Since p3webui and startupWebServices() (rsinit.cc:522) are the only in-tree callers of registerResourceProvider, a plugin-free build is unaffected — the provider set is empty at line 459 and nothing changes. But as soon as a plugin registers a provider, which #3289 does unconditionally in setInterfaces(), the server starts on every launch.

The user's consent flag never enters the picture. JsonApiPage::checkStartJsonApi() (retroshare-gui/src/gui/settings/JsonApiPage.cc:161) opens with if(!Settings->getJsonApiEnabled()) return false;, and that setting defaults to false (rsharesettings.cpp:1249). startupWebServices() never consults it. So a user who has never enabled the JSON API — the default state — ends up with it listening.

It also shows up in the settings page in a misleading way. JsonApiPage::load() (JsonApiPage.cc:135) does setChecked(rsJsonApi->isRunning()), so the checkbox reports "enabled" while the stored setting says disabled. load() uses whileBlocking(), so merely opening the page persists nothing — but the port spin box and the listen address field are wired to updateParams() (JsonApiPage.cc:45-46), which writes Settings->setJsonApiEnabled(ui.enableCheckBox->isChecked()) (JsonApiPage.cc:124). One edit to either field turns the user's stored "off" into a stored "on".

Finally, the address and port used by this automatic start come from jsonapi.cfg (loaded by connectToConfigManager()), not from the GUI settings: the GUI only fills conf.jsonApiPort / conf.jsonApiBindAddress from the -J / -P command line options (main.cpp:379-380). The default binding is 127.0.0.1 (rsjsonapi.h:137), so this is not LAN exposure out of the box — but a user who once set a non-loopback address gets it re-applied without being asked.

Presence of a resource provider is not the same thing as user intent. Either gate on the user setting (pass it through RsConfigOptions / force_start_jsonapi from the front end), or make the request explicit on the plugin side, but please do not infer "start a network service" from "a provider object exists". At minimum it should be logged distinctly rather than folded into the existing "Starting JSON API." line.

3. retroshare-service and Android are not actually fixed by this PR

JsonApiServer::run() (src/jsonapi/jsonapi.cpp:935) snapshots getResources() once when the thread starts and publishes that snapshot to restbed. A provider registered afterwards has no effect until a restart — restart()unProtectedRestart() (jsonapi.cpp:131) is fullstop() + RsThread::start(), which re-runs the snapshot.

In retroshare-service and Android, startupWebServices() runs before login with force_start_jsonapi = true, so the server is already running by the time setInterfaces() (rsinit.cc:1714) hands mJsonApi to the plugins. Their routes are registered into a live server and never published.

#3289 works around this plugin-side with if(mInterfaces.mJsonApi->isRunning()) mInterfaces.mJsonApi->restart(true);. That carries a cost the PR description does not mention: restart(true) spins until RESTART_BURST_PROTECTION7 seconds (src/jsonapi/jsonapi.h:234) — has elapsed since the previous restart, and startupWebServices() restarted the server about one second earlier (there is even an explicit rs_usleep(1000000) right after it, retroshare-service.cc:292). On an autologin start that is up to ~6 s of blocking inside setInterfaces(), on the startup path; and since mRestartReqTS is refreshed by each restart, the next plugin pays a fresh 7 s.

This belongs in the core rather than in every plugin: after mPluginsManager->setInterfaces(interfaces), if the server is running and the provider set changed, restart it once.


Important

4. mResourceProviders is unsynchronised

registerResourceProvider() / unregisterResourceProvider() (jsonapi.cpp:837 and :848) mutate a bare std::set with no mutex, while getResources() (jsonapi.cpp:866) iterates it from the JSON API thread and JsonApiPage::load() (JsonApiPage.cc:148) iterates it from the GUI thread. Until now the writers were p3webui and startupWebServices() — a small, mostly single-threaded set of callers. This PR opens it to arbitrary plugin code at arbitrary times, including while the server thread is live (the service/Android path of point 3). The member deserves the treatment mAuthTokenStorage already gets from configMutex.

5. Plugins receive the whole RsJsonApi, not just provider registration

Plugins are dlopened native code loaded with RTLD_NOW | RTLD_GLOBAL, so there is no real privilege boundary to break here — this is a design remark, not a vulnerability. Still, the interface handed over includes getAuthorizedTokens() (which returns user:password pairs in clear, the Web UI password among them), authorizeUser(), revokeAuthToken(), setBindingAddress(), setListeningPort() and fullstop(). If the contract is "a plugin may publish resources", a narrow interface exposing registerResourceProvider / unregisterResourceProvider / hasResourceProvider would express it better, and would let RsJsonApi keep evolving without dragging the plugin ABI along.

6. rsJsonApi != nullptr no longer means "configured"

The global is published at rsinit.cc:1694, well before startupWebServices() connects the config manager, loads the tokens and applies the port and binding address. Between those two points rsJsonApi is a live but unconfigured object. Existing code uses the null-ness of that pointer as a phase marker — see the comment at retroshare-service.cc:277 ("cannot be set using rsWebUI methods because it calls the still non-existent rsJsonApi") and src/jsonapi/p3webui.cc:226-249, which dereferences rsJsonApi with no null check at all. Nothing visibly breaks today, but the invariant is gone and nothing in the PR records that. A comment on the extern declaration, or an explicit "configured" state, would keep the next reader out of trouble.

7. The placement of the new block is load-bearing and undocumented

rsinit.cc:1362-1367 already contains:

#ifdef RS_JSONAPI
    if (rsJsonApi) { ... rsJsonApi->connectToConfigManager(*cfgmgr); }
#endif

The PR works only because the new creation (rsinit.cc:1692) sits after that block: in the GUI rsJsonApi is still null there, so the single connectToConfigManager() happens later, in startupWebServices(). Move the new block a few lines up and the config gets connected twice — p3ConfigMgr::addConfiguration() deduplicates by pointer (src/pqi/p3cfgmgr.cc:134) so it is not fatal, but it prints "Config already added" and connectToConfigManager() re-runs loadConfiguration() regardless. Either add a comment saying the order matters, or — cleaner — create the server before line 1362, let that pre-existing block own the config connection, and make startupWebServices() skip connectToConfigManager() when it is already done.


Minor

  • rsinit.cc:1696, interfaces.mJsonApi = nullptr; is a no-op: inited_ptr default-constructs to NULL (src/util/rsinitedptr.h:42). It does match the surrounding style (interfaces.mDht = NULL; in the neighbouring #else), so keeping it is fine — just noting it does nothing.
  • rsinit.cc:451: if rsJsonApi is non-null but not a JsonApiServer, the dynamic_cast yields null, a second server is created, and the global is silently overwritten while plugins still hold the previous pointer. Not reachable today, but an RsErr() in that branch costs one line.
  • rsinit.cc:455 rsJsonApi = jas; is redundant with the unchanged rsJsonApi = jas; at rsinit.cc:543. Harmless, but one of the two should go.
  • The JsonApiServer is now allocated on every run of an RS_JSONAPI build, including runs where the API is never started, and it is never deleted. The leak pre-exists; this PR makes the allocation unconditional.

What is good

  • The reuse via if(!rsJsonApi) correctly covers both startup orders (GUI after, service/Android before). That was the part most likely to be got wrong, and it is right.
  • A forward declaration rather than an #include in rsplugin.h is the correct call for a public header.
  • The loadList() merge semantics (jsonapi.cpp:805-810) mean a plugin calling authorizeUser() during setInterfaces() will not have its token wiped by the later config load. That interaction happens to be safe.
  • The added comments explain the why, not the what, which is what this file needed.

This PR was not compiled during the review; the findings come from reading the sources. Point 1 is reproducible by loading a .so built before the change with ALLOW_ALL_PLUGINS enabled.

- Moved mJsonApi to the end of RsPlugInInterfaces.
- Enforced RS_PLUGIN_API_VERSION; incompatible plugins now get PLUGIN_STATUS_WRONG_API.
- Added a GUI message for incompatible plugins.
- Removed automatic JSON API startup based only on provider presence.
- Removed FeedReader’s per-plugin startup restart.
- Core now restarts JSON API once after all plugins register, only when already running.
- Added mutex protection and provider snapshots for mResourceProviders.
@defnax

defnax commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

fix by chatgpt 5.6

Implemented the merge-blocking fixes in the working RetroShare source:

  • Moved mJsonApi to the end of RsPlugInInterfaces.
  • Enforced RS_PLUGIN_API_VERSION; incompatible plugins now get PLUGIN_STATUS_WRONG_API.
  • Added a GUI message for incompatible plugins.
  • Removed automatic JSON API startup based only on provider presence.
  • Removed FeedReader’s per-plugin startup restart.
  • Core now restarts JSON API once after all plugins register, only when already running.
  • Added mutex protection and provider snapshots for mResourceProviders.

Still suitable as later follow-up work:

  • Give plugins a narrow resource-registration interface instead of full RsJsonApi.
  • Add an explicit configured/initialized state.
  • Consider shared ownership for resource providers to make concurrent unregister/destruction fully lifetime-safe.

One consequence: after API-version enforcement, every plugin must be rebuilt against API version 0x000102; old plugin binaries will now be safely rejected instead of potentially crashing.

@jolavillette

Copy link
Copy Markdown
Contributor

Re-reviewed at fc6ee7fb6 (2 commits, base master), 6 files, +81/-14. Line numbers refer to the final state of the files in this PR. Still judged together with RetroShare/RetroShare#3289, since neither compiles or behaves correctly without the other.

Thanks for the follow-up commit — the four actionable points from the previous round are all addressed, and addressed properly rather than papered over. I re-checked each one against the diff:

  • RsPlugInInterfaces ABImJsonApi is now appended after mWire (src/retroshare/rsplugin.h:147), with a comment saying why it must stay appended. Older plugins keep every offset they know.
  • API version enforcementloadPlugin() rejects a mismatch (src/plugins/pluginmanager.cc:374-384), dlclose()s before returning, and reuses the existing PLUGIN_STATUS_WRONG_API (rsplugin.h:104) rather than inventing a code. The stale "Not used yet" comment was updated to match. The bump now means something. #3289 adds the matching GUI string in PluginsPage.cpp, so a rejected plugin reports "Incompatible plugin API version. Rebuild or update this plugin." instead of falling through to default: "Unknown status.".
  • Unconsented JSON API startjsonapi_needed is back to force_start_jsonapi (src/rsserver/rsinit.cc:457). Nothing starts a listening server on provider presence any more.
  • mResourceProviders lockingmutable RsMutex mResourceProvidersMutex (jsonapi.h:183), all four accessors guarded, and the two details that are easy to get wrong are right: the SERVICE_LIST_CHANGED events are posted outside the lock, and getResources() (jsonapi.cpp:882) iterates a copy so provider code is never called with the mutex held. Constructor init-list order matches declaration order, so no -Wreorder. The only out-of-tree consumer of the new by-value getResourceProviders() is JsonApiPage.cc:148, a range-for over the temporary — lifetime-extended, fine.

The core-owned single restart is in the right place conceptually, but as written it runs on the wrong thread in exactly the deployments it targets. That is the one blocker left, and there is a related issue in #3289's shutdown path.


Blocker

The core restart runs on the JSON API server thread on Android and on webui-driven service login

src/rsserver/rsinit.cc:1722-1729 calls rsJsonApi->restart(true) from inside RsServer::StartupRetroShare(). That function is reached from RsLoginHelper::attemptLogin() (rsinit.cc:2230) — and attemptLogin has a JSON API handler registered at src/jsonapi/jsonapi.cpp:287 which calls it synchronously at line 325. createLocationV2 (jsonapi.cpp:224) has the same shape.

restbed dispatches those handlers on the JSON API server thread itself: JsonApiServer::run() never sets a worker limit, restbed defaults to m_worker_limit = 0 (supportlibs/restbed/source/corvusoft/restbed/detail/settings_impl.hpp:43), and with limit == 0 Service::start() runs m_io_context->run() on the calling thread (service.cpp:170-175) — i.e. inside JsonApiServer::run().

So on Android, where retroshareserviceandroid.cpp:78 only calls startupWebServices(conf, true) and login is always driven through the JSON API, and on retroshare-service whenever the user logs in from the web interface rather than with -U, the new block executes on the server's own thread. restart(true)unProtectedRestart() (jsonapi.cpp:131) → fullstop():

  1. askForStop()onStopRequested()mService->stop(), stopping restbed from inside one of its own handlers;
  2. waitWhileStopping() detects the self-join (src/util/rsthreads.cc:150-160), prints an RsErr plus a stacktrace, and returns without waiting;
  3. RsThread::start() then finds mHasStopped == false and bails out with RS_ERR("attempt to start already running thread") and a second stacktrace (rsthreads.cc:235-237).

Net effect: the JSON API is stopped and never comes back, two stacktraces in the log, and the in-flight login response most likely never delivered. The client is left talking to a dead API — and the plugin routes this PR exists to publish are still not published, since these are precisely the two cases where jsonApiWasRunning can be true.

This is the hazard the existing /rsJsonApi/restart handler documents and works around, jsonapi.cpp:534-539:

/* Wrap inside RsThread::async because this call fullstop() on
 * JSON API server thread.
 * Calling RsThread::fullstop() from it's own thread should never
 * happen and if it happens an error message is printed
 * accordingly by RsThread::fullstop() */
if(!retval) RsThread::async([this](){ unProtectedRestart(); });

The same pattern applies here — wrap the new restart in RsThread::async, or skip it when the caller is already the server thread. That also gets the burst-protection wait off the startup path (see below). The terminal path (retroshare-service -U <id>) calls attemptLogin from main() and is unaffected; only API-driven logins hit this.


Related, in #3289: the restart moved from startup to shutdown

Flagging it here because the two PRs land together. The per-plugin startup restart was correctly removed from FeedReaderPlugin::setInterfaces(), but FeedReaderPlugin::stop() still does:

mInterfaces.mJsonApi->unregisterResourceProvider(*mJsonApiProvider);
if(mInterfaces.mJsonApi->isRunning())
    mInterfaces.mJsonApi->restart(true);

RsServer::rsGlobalShutDown() calls stopPlugins() at src/rsserver/p3face-config.cc:104 and rsJsonApi->fullstop() at line 127. So on every shutdown with the plugin loaded, the JSON API is torn down and brought back up — after waiting out up to 7 s of burst protection — a few lines before it is killed for good. It restarts an HTTP server in the middle of core teardown, re-publishing resources whose backing services are stopped immediately afterwards.

There is no self-join here: the /rsControl/rsGlobalShutDown handler already wraps the call in RsThread::async (jsonapi.cpp:361), so shutdown never runs on the server thread. But the restart itself has no purpose at that point. Unregistering is enough; the restart should just be dropped.


Minor

  • restart(true) still pays up to RESTART_BURST_PROTECTION = 7 s (src/jsonapi/jsonapi.h:234), since startupWebServices() restarted the server roughly a second earlier (retroshare-service.cc:292 even sleeps 1 s deliberately). It is now paid once instead of once per plugin, which was the point — but it is paid synchronously inside StartupRetroShare(). RsThread::async fixes that too; alternatively a core-internal restart could bypass the burst protection, which exists to stop API clients machine-gunning the endpoint, not to throttle the core.
  • Merge coupling is now two-way and worth a line in both descriptions: Changes for JSONAPI to work with plugins #364 alone leaves every plugin binary built before the bump reporting "Unknown status.", because the PLUGIN_STATUS_WRONG_API case lives in #3289's PluginsPage.cpp; #3289 alone does not compile, because mJsonApi does not exist yet. #3289's submodule pointer also has to reference the merged Changes for JSONAPI to work with plugins #364.
  • jsonApiProviderCount != getResourceProviders().size() misses a register+unregister pair that nets to zero across setInterfaces(). Theoretical, but comparing the sets — or just an "anything registered?" flag — would be exact and no more code.
  • getResources() copies the reference_wrapper set under the lock and dereferences the entries outside it. That is the right trade-off versus holding the lock across provider code, but nothing stops a provider being destroyed between the copy and rp.get().getResources()RsPluginManager::stopPlugins() deletes plugin objects. Pre-existing shape, not introduced here; worth a comment at most, or the shared-ownership follow-up you already have in mind.
  • rsjsonapi.h:211: the using ResourceProviderSet alias is inserted between the doc comment and the functions that comment documents, so the comment now reads as documentation for the alias. Moving the alias above the comment restores it.
  • Two points from the previous round are untouched and still stand, both one-liners: rsJsonApi != nullptr no longer implies "configured" (it is published at rsinit.cc:1692, before the config manager, tokens, port and address are applied, and p3webui.cc:226-249 dereferences it with no null check); and the new creation block's position after rsinit.cc:1362-1367 is load-bearing — move it up and connectToConfigManager() runs twice.
  • Also untouched, and fine to leave as follow-up: plugins receive the whole RsJsonApi, including getAuthorizedTokens(), which returns user:password in clear. Plugins are RTLD_GLOBAL native code so there is no boundary being crossed; a narrower register/unregister interface would just express the contract better and decouple the plugin ABI from RsJsonApi.

What is good

  • The four fixes are real fixes. Posting the events outside the mutex and copying the provider set before calling into provider code are the two details that separate a working lock from a deadlock waiting to happen, and both are right.
  • Appending the struct member with a comment saying why it must stay appended is what keeps the next contributor from undoing it.
  • The version check is placed after the dlopen, cleans up with dlclose(), and reuses an existing status code — and #3289 supplies the matching user-facing string, so the rejection is actionable rather than mysterious.
  • Requiring every plugin to be rebuilt against 0x000102 is the correct consequence, not a regression: old binaries are now refused instead of being handed a struct whose layout they disagree with.
  • Moving the restart into the core, conditional on the server actually running and on the provider set having changed, is the right ownership — it just needs to be dispatched off the server's own thread.

This PR was not compiled during the review; the findings come from reading the sources. The blocker reproduces by logging in from the web interface against a retroshare-service with a provider-registering plugin loaded, and shows up as the two stacktraces above in the log.

@defnax

defnax commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

maybe you can make it better, claude wont like then my prs

defnax and others added 3 commits August 11, 2026 20:21
StartupRetroShare() restarts the JSON API once after setInterfaces() so
plugin resource providers get published in retroshare-service and on
Android, where the server is already running by then.

Those are exactly the two setups where StartupRetroShare() can be reached
from the JSON API server thread: it is called by RsLoginHelper::attemptLogin()
and createLocationV2(), both exposed through the API, and restbed serves its
handlers on the thread that called Service::start() (no worker limit is set,
so restbed defaults to running the io_context inline) -- that thread being
JsonApiServer::run().

restart() calls RsThread::fullstop(). Joining our own thread makes
waitWhileStopping() print an error and return without waiting, and the
following RsThread::start() then fails with "attempt to start already running
thread". The server ends up stopped for good, the in-flight login response is
never delivered, and the plugin routes this restart exists to publish are
still missing.

Wrap the restart in RsThread::async, the same way the /rsJsonApi/restart
handler already does for the same reason. As a side effect the
RESTART_BURST_PROTECTION wait, which throttles API clients rather than the
core, no longer blocks startup.
No behaviour change, three comment-level fixes from the review:

- rsjsonapi.h: the ResourceProviderSet alias had been inserted between the
  doc comment and the functions that comment documents, so the comment read
  as documentation for the alias. Move the alias above it and note that
  getResourceProviders() now returns a snapshot by value.

- rsjsonapi.h: rsJsonApi being non-null no longer implies the server is
  configured, since StartupRetroShare() now publishes it before
  startupWebServices() applies the config manager, tokens, port and binding
  address. Existing code uses the null-ness of that pointer as a phase
  marker, so say it on the declaration.

- rsinit.cc: the creation block has to stay after the pre-existing
  connectToConfigManager() block, otherwise the GUI connects the config
  manager twice and reloads jsonapi.cfg twice. That ordering was load
  bearing and undocumented.
defnax and others added 5 commits August 11, 2026 23:22
…read

Dispatch the post-plugin JSON API restart off the API server thread
rsGlobalShutDown() stopped the JSON API almost last, after stopPlugins().
A plugin that registered a JsonApiResourceProvider deletes it in its stop(),
but the running restbed service still holds the restbed::Resource objects
that provider returned, and their handlers capture it. Any request served
between stopPlugins() and the fullstop at the end of the function therefore
dereferences freed memory.

The window is not theoretical: everything in between -- UPnP teardown, the
auto-proxy shutdown, all registered service threads, the RsServer tick
thread and the per-peer streamers -- can take tens of seconds, and a web
interface polls throughout.

Move the fullstop to the top of the function. It also keeps an API client
from touching the configuration after ConfigFinalSave(), and it must stay
outside the wasReady branch: retroshare-service and Android start the JSON
API before login, so a shutdown from that state has to stop it too.

Without this, a plugin has to restart the whole JSON API from its stop() to
make deleting its own provider safe, which costs a burst-protection wait and
brings the server back up in the middle of teardown.
…r-364

Stop the JSON API before plugins delete their resource providers
startupWebServices() casts rsJsonApi to JsonApiServer to reuse the instance the
plugin handoff created. If that cast ever fails, the previous code built a
second server and overwrote the global without a word -- while every plugin
still holds the pointer it was handed in setInterfaces(), now pointing at an
object nobody drives. Unreachable as things stand, since rsJsonApi is only ever
set to a JsonApiServer; worth one line of log rather than a silent swap.

The trailing `rsJsonApi = jas;` at the end of the function repeated what the
same function already did on the line above the cast, and only when it had
created the server itself. Dropped.
…or-364

jsonapi: do not replace rsJsonApi silently, and assign it once
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants