Page MenuHomePhabricator

SMW triggers "Sessions are disabled for this entry point"
Closed, DeclinedPublic

Description

2016-05-06 10:37:03 translatewiki.net translatewiki_net-bw_: [db3f3b2f216ed53e3b39849a] /w/load.php?debug=false&lang=zh-hans&modules=startup&only=scripts&skin=vector   BadMethodCallException from line 988 of /srv/mediawiki/tags/2016-05-06_10:28:35/includes/session/SessionManager.php: Sessions are disabled for this entry point
#0 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/session/SessionManager.php(187): MediaWiki\Session\SessionManager->getSessionFromInfo(MediaWiki\Session\SessionInfo, WebRequest)
#1 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/WebRequest.php(700): MediaWiki\Session\SessionManager->getSessionForRequest(WebRequest)
#2 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/user/User.php(1211): WebRequest->getSession()
#3 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/user/User.php(405): User->loadFromSession()
#4 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/user/User.php(5169): User->load()
#5 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/user/User.php(2793): User->loadOptions()
#6 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/context/RequestContext.php(363): User->getOption(string)
#7 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/Message.php(672): RequestContext->getLanguage()
#8 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/context/RequestContext.php(457): Message->setContext(RequestContext)
#9 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/context/ContextSource.php(190): RequestContext->msg(string)
#10 /srv/mediawiki/tags/2016-05-06_10:28:35/extensions/SemanticMediaWiki/includes/queryprinters/TableResultPrinter.php(35): ContextSource->msg(string)
#11 /srv/mediawiki/tags/2016-05-06_10:28:35/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderGetConfigVars.php(62): SMW\TableResultPrinter->getName()
#12 /srv/mediawiki/tags/2016-05-06_10:28:35/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php(341): SMW\MediaWiki\Hooks\ResourceLoaderGetConfigVars->process()
#13 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/Hooks.php(195): Closure$SMW\MediaWiki\Hooks\HookRegistry::addCallbackHandlers#15(array)
#14 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoaderStartUpModule.php(117): Hooks::run(string, array)
#15 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoaderStartUpModule.php(375): ResourceLoaderStartUpModule->getConfigSettings(DerivativeResourceLoaderContext)
#16 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoaderModule.php(707): ResourceLoaderStartUpModule->getDefinitionSummary(DerivativeResourceLoaderContext)
#17 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoader.php(622): ResourceLoaderModule->getVersionHash(DerivativeResourceLoaderContext)
#18 [internal function]: Closure$ResourceLoader::getCombinedVersion(string)
#19 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoader.php(623): array_map(Closure$ResourceLoader::getCombinedVersion;467806508, array)
#20 /srv/mediawiki/tags/2016-05-06_10:28:35/includes/resourceloader/ResourceLoader.php(675): ResourceLoader->getCombinedVersion(ResourceLoaderContext, array)
#21 /srv/mediawiki/tags/2016-05-06_10:28:35/load.php(46): ResourceLoader->respond(ResourceLoaderContext)
#22 {main}

Event Timeline

Restricted Application added a subscriber: Zppix. · View Herald TranscriptMay 6 2016, 10:41 AM
Tgr added a comment.May 6 2016, 12:03 PM

The ResourceLoaderGetConfigVars hook calls SMW\TableResultPrinter::getName which uses ContextSource::msg to create the message, and that creates a session user to figure out which language to use for a message. That's invalid for ResourceLoaderGetConfigVars as it will be cached and served to all users so it cannot depend on the context. Either getName should be changed to or replaced with something that uses the content language (ie. replace $this->msg('...') with wfMessage('...')), or the variable should be defined in the MakeGlobalVariablesScript hook instead.

Thanks for looking into this. Hopefully this will help the developers to provide a fix.

The ResourceLoaderGetConfigVars hook calls SMW\TableResultPrinter::getName which uses ContextSource::msg to create the message, and that creates a session user to figure out which language to use for a message. That's invalid for ResourceLoaderGetConfigVars as it will be cached and served to all users

That's not entirely accurate. ResourceLoaderContext does vary by user language (not user session, but user language, as query parameter passed from the page view). So varying on user language is fine, but requires it to use ResourceLoaderContext::msg rather than RequestContext::msg.

The upstream task is marked as fixed. Perhaps this can be closed as well.

Tgr added a comment.May 10 2016, 1:34 PM

So varying on user language is fine, but requires it to use ResourceLoaderContext::msg rather than RequestContext::msg.

Would it be possible to have a RequestContext subclass that replaces the normal one on load.php pages and takes language, skin and whatever else is applicable from the RL context instead of the session user? That would probably get rid of most of the "sessions disabled" errors.

Would it be possible to have a RequestContext subclass that replaces the normal one on load.php pages and takes language, skin and whatever else is applicable from the RL context instead of the session user? That would probably get rid of most of the "sessions disabled" errors.

That's about the same as T130720: Have load.php set $wgLang and the RequestContext language, which was declined.

The ResourceLoaderGetConfigVars hook calls SMW\TableResultPrinter::getName which uses ContextSource::msg to create the message, and that creates a session user to figure out which language to use for a message. That's invalid for ResourceLoaderGetConfigVars as it will be cached and served to all users

That's not entirely accurate. ResourceLoaderContext does vary by user language (not user session, but user language, as query parameter passed from the page view). So varying on user language is fine, but requires it to use ResourceLoaderContext::msg rather than RequestContext::msg.

I note that the ResourceLoaderGetConfigVars hook doesn't get passed a ResourceLoaderContext.

Krinkle removed a subscriber: Krinkle.May 10 2016, 2:55 PM

The general issue stills pops out here and then in somewhat different forms. I saw it happen with visual editor, and now with mobile frontend (both when parsing a message):

2016-09-21 08:42:47 translatewiki.net dev_translatewiki_net-bw_: [79dcb8da769fe5d1716782cb] /w/load.php?debug=false&lang=fi&modules=mobile.search%7Cskins.minerva.notifications%2Cscripts%2Ctoggling&skin=minerva&version=1ytnjkt   BadMethodCallException from line 845 of /www/dev.translatewiki.net/docroot/w/includes/session/SessionManager.php: Sessions are disabled for this entry point
#0 /www/dev.translatewiki.net/docroot/w/includes/session/SessionManager.php(194): MediaWiki\Session\SessionManager->getSessionFromInfo(MediaWiki\Session\SessionInfo, WebRequest)
#1 /www/dev.translatewiki.net/docroot/w/includes/WebRequest.php(734): MediaWiki\Session\SessionManager->getSessionForRequest(WebRequest)
#2 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(1198): WebRequest->getSession()
#3 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(403): User->loadFromSession()
#4 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(5120): User->load()
#5 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(2763): User->loadOptions()
#6 /www/dev.translatewiki.net/docroot/w/includes/context/RequestContext.php(363): User->getOption(string)
#7 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(207): RequestContext->getLanguage()
#8 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(169): StubUserLang->_newObject()
#9 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(112): StubObject->_unstub(string, integer)
#10 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(139): StubObject->_call(string, array)
#11 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/DataTypeRegistry.php(127): StubObject->__call(string, array)
#12 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/DataValueFactory.php(58): SMW\DataTypeRegistry::getInstance()
#13 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/InTextAnnotationParser.php(88): SMW\DataValueFactory::getInstance()
#14 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/ApplicationFactory.php(235): SMW\InTextAnnotationParser->__construct(SMW\ParserData, SMW\MediaWiki\MagicWordsFinder, SMW\MediaWiki\RedirectTargetFinder)
#15 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php(108): SMW\ApplicationFactory->newInTextAnnotationParser(SMW\ParserData)
#16 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php(62): SMW\MediaWiki\Hooks\InternalParseBeforeLinks->performUpdate()
#17 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php(204): SMW\MediaWiki\Hooks\InternalParseBeforeLinks->process()
#18 /www/dev.translatewiki.net/docroot/w/includes/Hooks.php(195): Closure$SMW\MediaWiki\Hooks\HookRegistry::addCallbackHandlers#6(Parser, string, StripState)
#19 /www/dev.translatewiki.net/docroot/w/includes/parser/Parser.php(1262): Hooks::run(string, array)
#20 /www/dev.translatewiki.net/docroot/w/includes/parser/Parser.php(440): Parser->internalParse(string)
#21 /www/dev.translatewiki.net/docroot/w/includes/cache/MessageCache.php(1103): Parser->parse(string, Title, ParserOptions, boolean)
#22 /www/dev.translatewiki.net/docroot/w/includes/Message.php(1146): MessageCache->parse(string, Title, boolean, boolean, LanguageFi)
#23 /www/dev.translatewiki.net/docroot/w/includes/Message.php(828): Message->parseText(string)
#24 /www/dev.translatewiki.net/docroot/w/includes/Message.php(884): Message->toString()
#25 /www/dev.translatewiki.net/docroot/w/extensions/MobileFrontend/includes/modules/MFResourceLoaderParsedMessageModule.php(47): Message->parse()
#26 /www/dev.translatewiki.net/docroot/w/extensions/MobileFrontend/includes/modules/MFResourceLoaderParsedMessageModule.php(78): MFResourceLoaderParsedMessageModule->addParsedMessages(ResourceLoaderContext)
#27 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(626): MFResourceLoaderParsedMessageModule->getScript(ResourceLoaderContext)
#28 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(594): ResourceLoaderModule->buildContent(ResourceLoaderContext)
#29 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(1011): ResourceLoaderModule->getModuleContent(ResourceLoaderContext)
#30 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(736): ResourceLoader->makeModuleResponse(ResourceLoaderContext, array, array)
#31 /www/dev.translatewiki.net/docroot/w/load.php(46): ResourceLoader->respond(ResourceLoaderContext)
#32 {main}

Would someone have any thoughts where this issue could/should be fixed?

MobileFrontend in that stack trace is doing what it's supposed to do: it's loading the message via the ResourceLoaderContext, which sets the correct language.

SMW is breaking things in that trace by attempting to access the session user's language during the parsing of the wikitext of a message in a context (load.php) where there is no session and therefore no session user. Chances are it should be using the parser instance's target language instead of the session user's language.

Based on the above comment, I made a new "upstream" report: https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/1867

Tgr closed this task as Declined.Nov 22 2016, 4:11 AM

Upstream was closed, and I don't think there is anything we can do about this in core (unless we change our mind on T130720).

Upstream was closed, and I don't think there is anything we can do about this in core (unless we change our mind on T130720).

Answering the questions in the upstream task would have been nice. I think they are still worth looking, there might be an issue in core.

It seems they found a working work-around.

Answering the questions in the upstream task would have been nice. I think they are still worth looking, there might be an issue in core.

The questions were never asked here. Looking at them:

especially given that in previous releases there were no such issues and I'm leaning towards the introduction of the SessionManager to be the main reason for said issue ( #1867 comes to mind as well).

The change that "caused" this was the enforcing of the policy that load.php should not depend in any way on session data. T127233: Endpoints which do not need to authenticate users should set MW_NO_SESSION is the task about enforcing that for load.php and other endpoints, which came about due to discussion on T126700: Severe save latency regression.

Normally, when the InternalParseBeforeLinks hook is triggered SMW checks whether to proceed or not by looking at ParserOptions::getInterfaceMessage [0, 1] which is expected to be set when Message::parse is used

ParserOptions::getInterfaceMessage is set when parsing a Message object with the interface flag set. As far as I can tell, this flag is supposed to indicate whether the message is being parsed for site chrome (true) or for embedding in the page content (false). Examples of the latter include error messages from Scribunto invokes and Cite ref tags. In practice, what it usually really indicates is whether $message->inLanguage() or $message->inContentLanguage() was called because hardly anything seems to set the flag explicitly.

What it seems to control in core is whether the {{gender:}} magic word defaults to ParserOptions::getUser(), whether Parser::getTargetLanguage() falls back to the user language in ParserOptions if no target language was explicitly set in the ParserOptions (but MessageCache always does set a target language), and whether language conversion is skipped.

At any rate, it's certainly not intended to be an indicator of whether the parse operation is coming from Message::parse() or not. I don't know of any such indicator offhand.

At any rate, it's certainly not intended to be an indicator of whether the parse operation is coming from Message::parse() or not. I don't know of any such indicator offhand.

It means that one has no chance of making a reliable judgement whether a parse process was initiated from a content and not from an auxiliary "process" such as Message::parse.

I would rely on another option (if available and the initiating parse process is marked as such) to decide whether it is a content/auxiliary parse or not to avoid unnecessary processing in mentioned cases.

Tgr added a comment.Nov 22 2016, 10:04 PM

I ran into that problem two years ago and at the time there wasn't any clean solution (I ended up adding a new hook for my usecase).

I ran into that problem two years ago and at the time there wasn't any clean solution (I ended up adding a new hook for my usecase).

I did a quick test but this doesn't work for our use case (InternalParseBeforeLinks, ParserAfterTidy hook) to decided whether to proceed or not. We require the raw text to apply necessary changes before the parse process is finalized and not knowing the origin of the parse, eventually forces us to execute the hook call no matter what.

I added a recent trace

1[1] 2016-12-19 14:48:40 translatewiki.net dev_translatewiki_net-bw_: [054b6d1aa0d6a01001489f6e] /w/load.php?debug=false&lang=fi&modules=startup&only=scripts&skin=vector BadMethodCallException from line
2846 of /www/dev.translatewiki.net/docroot/w/includes/session/SessionManager.php: Sessions are disabled for this entry point
3#0 /www/dev.translatewiki.net/docroot/w/includes/session/SessionManager.php(195): MediaWiki\Session\SessionManager->getSessionFromInfo(MediaWiki\Session\SessionInfo, WebRequest)
4#1 /www/dev.translatewiki.net/docroot/w/includes/WebRequest.php(735): MediaWiki\Session\SessionManager->getSessionForRequest(WebRequest)
5#2 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(1188): WebRequest->getSession()
6#3 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(408): User->loadFromSession()
7#4 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(5150): User->load()
8#5 /www/dev.translatewiki.net/docroot/w/includes/user/User.php(2779): User->loadOptions()
9#6 /www/dev.translatewiki.net/docroot/w/includes/context/RequestContext.php(364): User->getOption(string)
10#7 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(207): RequestContext->getLanguage()
11#8 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(169): StubUserLang->_newObject()
12#9 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(112): StubObject->_unstub(string, integer)
13#10 /www/dev.translatewiki.net/docroot/w/includes/StubObject.php(139): StubObject->_call(string, array)
14#11 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/DataTypeRegistry.php(127): StubObject->__call(string, array)
15#12 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/DataValueFactory.php(58): SMW\DataTypeRegistry::getInstance()
16#13 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/InTextAnnotationParser.php(88): SMW\DataValueFactory::getInstance()
17#14 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/ApplicationFactory.php(235): SMW\InTextAnnotationParser->__construct(SMW\ParserData, SMW\MediaWiki\MagicWordsFinder, SMW\Medi
18aWiki\RedirectTargetFinder)
19#15 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php(108): SMW\ApplicationFactory->newInTextAnnotationParser(SMW\ParserData)
20#16 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php(62): SMW\MediaWiki\Hooks\InternalParseBeforeLinks->performUpdate()
21#17 /www/dev.translatewiki.net/docroot/w/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php(204): SMW\MediaWiki\Hooks\InternalParseBeforeLinks->process()
22#18 /www/dev.translatewiki.net/docroot/w/includes/Hooks.php(195): Closure$SMW\MediaWiki\Hooks\HookRegistry::addCallbackHandlers#6(Parser, string, StripState)
23#19 /www/dev.translatewiki.net/docroot/w/includes/parser/Parser.php(1277): Hooks::run(string, array)
24#20 /www/dev.translatewiki.net/docroot/w/includes/parser/Parser.php(441): Parser->internalParse(string)
25#21 /www/dev.translatewiki.net/docroot/w/includes/cache/MessageCache.php(1138): Parser->parse(string, Title, ParserOptions, boolean)
26#22 /www/dev.translatewiki.net/docroot/w/includes/Message.php(1221): MessageCache->parse(string, Title, boolean, boolean, LanguageFi)
27#23 /www/dev.translatewiki.net/docroot/w/includes/Message.php(869): Message->parseText(string)
28#24 /www/dev.translatewiki.net/docroot/w/includes/Message.php(922): Message->toString(string)
29#25 /www/dev.translatewiki.net/docroot/w/includes/EditPage.php(3437): Message->parse()
30#26 /www/dev.translatewiki.net/docroot/w/extensions/VisualEditor/VisualEditorDataModule.php(59): EditPage::getCopyrightWarning(Title, string, string)
31#27 /www/dev.translatewiki.net/docroot/w/extensions/VisualEditor/VisualEditorDataModule.php(21): VisualEditorDataModule->getMessageInfo(DerivativeResourceLoaderContext)
32#28 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(627): VisualEditorDataModule->getScript(DerivativeResourceLoaderContext)
33#29 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(595): ResourceLoaderModule->buildContent(DerivativeResourceLoaderContext)
34#30 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(743): ResourceLoaderModule->getModuleContent(DerivativeResourceLoaderContext)
35#31 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(641): ResourceLoaderModule->getVersionHash(DerivativeResourceLoaderContext)
36#32 [internal function]: Closure$ResourceLoader::getCombinedVersion(string)
37#33 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(654): array_map(Closure$ResourceLoader::getCombinedVersion;1990566860, array)
38#34 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderStartUpModule.php(423): ResourceLoader->getCombinedVersion(DerivativeResourceLoaderContext, array)
39#35 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderStartUpModule.php(396): ResourceLoaderStartUpModule->getAllModuleHashes(DerivativeResourceLoaderContext)
40#36 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoaderModule.php(746): ResourceLoaderStartUpModule->getDefinitionSummary(DerivativeResourceLoaderContext)
41#37 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(641): ResourceLoaderModule->getVersionHash(DerivativeResourceLoaderContext)
42#38 [internal function]: Closure$ResourceLoader::getCombinedVersion(string)
43#39 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(654): array_map(Closure$ResourceLoader::getCombinedVersion;1990566860, array)
44#40 /www/dev.translatewiki.net/docroot/w/includes/resourceloader/ResourceLoader.php(734): ResourceLoader->getCombinedVersion(ResourceLoaderContext, array)
45#41 /www/dev.translatewiki.net/docroot/w/load.php(53): ResourceLoader->respond(ResourceLoaderContext)
46#42 {main}

Tgr added a comment.Dec 19 2016, 7:51 PM

What version is that? master DataTypeRegistry.php#L127 does not seem to do any unstubbing.

I'm using 2.4.4 currently. I can check with master version tomorrow.

Okay, I'm not getting that error with master version. I had not seen any comments about this being fixed, so I was surprised that it seems to work for now, after a next release.

I had not seen any comments about this being fixed, so I was surprised that it seems to work for now, after a next release.

I noted in [0] on 16 Oct that:

Since the InternalParseBeforeLinks hook is executed (which it shouldn't given above remarks) and in anticipation of possible implications for future changes infused by developers, I provisionally removed the call to the user language from DataTypeRegistry.php(127): StubObject->__call(string, array) in [3] which will only work with changes forthcoming in #1879 and therefore are unlikely to be backported to 2.4

[0] https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/1867#issuecomment-254060896

Tgr added a comment.Dec 21 2016, 1:29 AM

So to recap, there are two ways to deal with this:

  • add guard conditions in SMW to every user/lang check that might happen before setup is over and/or in a ResourceLoader context (User::isSafeToLoad(); if you are not dealing with a user object, just copy the condition from that method)
  • ask for T130720 to be reopened and resolved differently

Either case, there is nothing actionable in this task anymore.

James indicated earlier that SMW would need to know whether the parser is working on a page or a message to decide how to do language selection (I think? I didn't fully understand); if that's the case, it should be filed as a new bug. I'm not that familiar with the parser but I imagine it wouldn't be hard to add; certainly seems like a sane feature to have.