/** @short define the current cache entry, which should be used for current
backup or cleanUp operation ... which is may be done asynchronous */
sal_Int32 m_nWorkingEntryID;
/** @short used for async operations, to prevent us from dying.
/// default state, if a document was new created or loaded
Unknown = 0, /// modified against the original file
Modified = 1, /// an active document can be postponed to be saved later.
Postponed = 2, /// was already handled during one AutoSave/Recovery session.
Handled = 4, /** an action was started (saving/loading) ... Can be interesting later if the process may be was interrupted by an exception. */
TrySave = 8,
TryLoadBackup = 16,
TryLoadOriginal = 32,
/* FINAL STATES */
/// the Auto/Emergency saved document is not usable any longer
Damaged = 64, /// the Auto/Emergency saved document is not really up-to-date (some changes can be missing)
Incomplete = 128, /// the Auto/Emergency saved document was processed successfully
Succeeded = 512
};
class AutoRecovery : private cppu::BaseMutex
, public AutoRecovery_BASE
, public ::cppu::OPropertySetHelper // => XPropertySet, XFastPropertySet, XMultiPropertySet
{ public: /** @short indicates the results of a FAILURE_SAFE operation
// TODO document me enum ETimerType
{ /** the timer should not be used next time */
E_DONT_START_TIMER, /** timer (was/must be) started with normal AutoSaveTimeIntervall */
E_NORMAL_AUTOSAVE_INTERVALL, /** timer must be started with special short time interval,
to poll for an user idle period */
E_POLL_FOR_USER_IDLE, /** timer must be started with a very(!) short time interval,
to poll for the end of an user action, which does not allow saving documents in general */
E_POLL_TILL_AUTOSAVE_IS_ALLOWED, /** don't start the timer - but calls the same action then before immediately again! */
E_CALL_ME_BACK
};
/** @short combine different information about one office document. */ struct TDocumentInfo
{ public:
/** Because our applications not ready for concurrent save requests at the same time, wehavesuppressourownAutoSaveforthemoment,adocumentwillbealreadysaved byothers.
*/ bool UsedForSaving;
/** For every user action, which modifies a document (e.g. key input) we get anotificationasXModifyListener.Thatseemstobea"performanceissue".-) Sowedecidedtolistenforsuchmodifyeventsonlyforthetimeinwhichthedocument wasstoredastemp.fileandwasnotmodifiedagainbytheuser.
*/ bool ListenForModify;
/** For SessionSave we must close all open documents by ourself. Butbecausewearelistenfordocumentsevents,wegetsome... andderegisterthesedocumentsfromourconfiguration. That'swhywemarkthesedocumentsas"Closedbyourself"sowecan ignorethese"OnUnload"ordisposing()events.-)
*/ bool IgnoreClosing;
OUString OldTempURL; // previous recovery file (filename_0.odf) which will be removed
OUString NewTempURL; // new recovery file (filename_1.odf) that is being created
OUString AppModule; // e.g. com.sun.star.text.TextDocument - used to identify app module
OUString FactoryService; // the service to create a document of the module
OUString RealFilter; // real filter, which was used at loading time
OUString DefaultFilter; // supports saving of the default format without losing data
OUString Extension; // file extension of the default filter
OUString Title; // can be used as "DisplayName" on every recovery UI!
css::uno::Sequence< OUString >
ViewNames; // names of the view which were active at emergency-save time
sal_Int32 ID;
};
/** @short used to know every currently open document. */ typedef ::std::vector< TDocumentInfo > TDocumentList;
// member
private:
/** @short the global uno service manager. @descrMustbeusedtocreateownneededservices.
*/
css::uno::Reference< css::uno::XComponentContext > m_xContext;
/** @short points to the underlying recovery configuration. @descrThisinstancedoesnotcache-itcallsdirectlythe configurationAPI!
*/
css::uno::Reference< css::container::XNameAccess > m_xRecoveryCFG;
/** @short proxy weak binding to forward Events to ourself without anownershipcycle
*/
css::uno::Reference< css::util::XChangesListener > m_xRecoveryCFGListener;
/** @short points to the used configuration package or.openoffice.Setup @descrThisinstancedoesnotcache-itcallsdirectlythe configurationAPI!
*/
css::uno::Reference< css::container::XNameAccess > m_xModuleCFG;
/** @short holds the global event broadcaster alive, wherewelistenfornewcreateddocuments.
*/
css::uno::Reference< css::frame::XGlobalEventBroadcaster > m_xNewDocBroadcaster;
/** @short proxy weak binding to forward Events to ourself without anownershipcycle
*/
css::uno::Reference< css::document::XDocumentEventListener > m_xNewDocBroadcasterListener;
/** @short because we stop/restart listening sometimes, it's a good idea to know ifwealreadyregisteredaslistener.-)
*/ bool m_bListenForDocEvents; bool m_bListenForConfigChanges;
/** @short for an asynchronous operation we must know, if there is atleastonerunningjob(maybeasynchronous!).
*/
Job m_eJob;
/** @short the timer, which is used to be informed about the next savingtime... @remarkmustlockSolarMutextouse
*/
Timer m_aTimer;
/** @short make our dispatch asynchronous ... if required to do so! */
std::unique_ptr<vcl::EventPoster> m_xAsyncDispatcher;
/** @short indicates, which time period is currently used by the internaltimer.
*/
ETimerType m_eTimerType;
/** @short this cache is used to hold all information about recovery/emergencysavedocumentsalive.
*/
TDocumentList m_lDocCache;
// TODO document me
sal_Int32 m_nIdPool;
/** @short contains all status listener registered at this instance.
*/
comphelper::OMultiTypeInterfaceContainerHelperVar3<css::frame::XStatusListener, OUString> m_lListener;
/** @descr This member is used to prevent us against re-entrance problems. Amutexcan'thelptopreventusfromconcurrentusingofmembers insidethesamethread.Bute.g.ourinternallyusedstlstructures arenotthreadsafe...andfurthermoretheycan'tbeusedatthesametime foriterationandadd/removerequests! Sowehavetodetectsuchstatesand...showawarning. Maybetherewillbeabettersolutionnexttime...(copyingthecachetemp. bevorusing).
/** @descr These members are used to check the minimum disc space, which must exists tostartthecorrespondingoperation.
*/
sal_Int32 m_nMinSpaceDocSave;
sal_Int32 m_nMinSpaceConfigSave;
// TODO document me void implts_resetHandleStates();
// TODO document me void implts_specifyDefaultFilterAndExtension(AutoRecovery::TDocumentInfo& rInfo);
// TODO document me void implts_specifyAppModuleAndFactory(AutoRecovery::TDocumentInfo& rInfo);
/** retrieves the names of all active views of the given document @paramrInfo thedocumentinfo,whose<code>Document</code>membermustnotbe<NULL/>.
*/ void implts_collectActiveViewNames( AutoRecovery::TDocumentInfo& rInfo );
/** updates the configuration so that for all documents, their current view/names are stored
*/ void implts_persistAllActiveViewNames();
// TODO document me void implts_prepareEmergencySave();
// TODO document me void implts_doEmergencySave(const DispatchParams& aParams);
// TODO document me void implts_doRecovery(const DispatchParams& aParams);
// TODO document me void implts_doSessionSave(const DispatchParams& aParams);
// TODO document me void implts_doSessionQuietQuit();
// TODO document me void implts_doSessionRestore(const DispatchParams& aParams);
// TODO document me void implts_backupWorkingEntry(const DispatchParams& aParams);
// TODO document me void implts_cleanUpWorkingEntry(const DispatchParams& aParams);
/** try to make sure that all changed config items (not our used configaccessonly)willbeflushedbacktodisc.
constexpr OUString CMD_DO_AUTO_SAVE = u"/doAutoSave"_ustr; // force AutoSave ignoring the AutoSave timer
constexpr OUString CMD_DO_PREPARE_EMERGENCY_SAVE = u"/doPrepareEmergencySave"_ustr; // prepare the office for the following EmergencySave step (hide windows etcpp.)
constexpr OUString CMD_DO_EMERGENCY_SAVE = u"/doEmergencySave"_ustr; // do EmergencySave on crash
constexpr OUString CMD_DO_RECOVERY = u"/doAutoRecovery"_ustr; // recover all crashed documents
constexpr OUString CMD_DO_ENTRY_BACKUP = u"/doEntryBackup"_ustr; // try to store a temp or original file to a user defined location
constexpr OUString CMD_DO_ENTRY_CLEANUP = u"/doEntryCleanUp"_ustr; // remove the specified entry from the recovery cache
constexpr OUString CMD_DO_SESSION_SAVE = u"/doSessionSave"_ustr; // save all open documents if e.g. a window manager closes an user session
constexpr OUString CMD_DO_SESSION_QUIET_QUIT = u"/doSessionQuietQuit"_ustr; // let the current session be quietly closed ( the saving should be done using doSessionSave previously ) if e.g. a window manager closes an user session
constexpr OUString CMD_DO_SESSION_RESTORE = u"/doSessionRestore"_ustr; // restore a saved user session from disc
constexpr OUString CMD_DO_DISABLE_RECOVERY = u"/disableRecovery"_ustr; // disable recovery and auto save (!) temp. for this office session
constexpr OUString CMD_DO_SET_AUTOSAVE_STATE = u"/setAutoSaveState"_ustr; // disable/enable auto save (not crash save) for this office session
const sal_Int32 MIN_DISCSPACE_DOCSAVE = 5; // [MB] const sal_Int32 MIN_DISCSPACE_CONFIGSAVE = 1; // [MB] const sal_Int32 RETRY_STORE_ON_FULL_DISC_FOREVER = 300; // not forever ... but often enough .-) const sal_Int32 RETRY_STORE_ON_MIGHT_FULL_DISC_USEFULL = 3; // in case FULL DISC does not seem the real problem const sal_Int32 GIVE_UP_RETRY = 1; // in case FULL DISC does not seem the real problem
#define MIN_TIME_FOR_USER_IDLE 10000// 10s user idle
// enable the following defines in case you wish to simulate a full disc for debug purposes .-)
// this define throws every time a document is stored or a configuration change // should be flushed an exception ... so the special error handler for this scenario is triggered // #define TRIGGER_FULL_DISC_CHECK
// force "return sal_False" for the method impl_enoughDiscSpace(). // #define SIMULATE_FULL_DISC
class CacheLockGuard
{ private:
// holds the outside caller alive, so it's shared resources // are valid every time
css::uno::Reference< css::uno::XInterface > m_xOwner;
// mutex shared with outside caller!
osl::Mutex& m_rSharedMutex;
// this variable knows the state of the "cache lock"
sal_Int32& m_rCacheLock;
// to prevent increasing/decreasing of m_rCacheLock more than once // we must know if THIS guard has an actual lock set there! bool m_bLockedByThisGuard;
// This cache lock is needed only to prevent us from removing/adding // items from/into the recovery cache ... during it's used at another code place // for iterating .-)
// Modifying of item properties is allowed and sometimes needed! // So we should detect only the dangerous state of concurrent add/remove // requests and throw an exception then ... which can of course break the whole // operation. On the other side a crash reasoned by an invalid stl iterator // will have the same effect .-)
if ( (m_rCacheLock > 0) && bLockForAddRemoveVectorItems )
{
OSL_FAIL("Re-entrance problem detected. Using of an stl structure in combination with iteration, adding, removing of elements etcpp."); throw css::uno::RuntimeException(
u"Re-entrance problem detected. Using of an stl structure in combination with iteration, adding, removing of elements etcpp."_ustr,
m_xOwner);
}
if (m_rCacheLock < 0)
{
OSL_FAIL("Wrong using of member m_nDocCacheLock detected. A ref counted value shouldn't reach values <0 .-)"); throw css::uno::RuntimeException(
u"Wrong using of member m_nDocCacheLock detected. A ref counted value shouldn't reach values <0 .-)"_ustr,
m_xOwner);
} /* SAFE */
}
void AutoRecovery::initListeners()
{ // read configuration to know if autosave/recovery is on/off etcpp...
implts_readConfig();
implts_startListening();
// establish callback for our internal used timer. // Note: Its only active, if the timer will be started ...
SolarMutexGuard g;
m_aTimer.SetInvokeHandler(LINK(this, AutoRecovery, implts_timerExpired));
}
// still running operation ... ignoring Job::AutoSave. // All other requests has higher prio! if (
( m_eJob != Job::NoJob ) &&
((m_eJob & Job::AutoSave ) != Job::AutoSave)
)
{
SAL_INFO("fwk.autorecovery", "AutoRecovery::dispatch(): There is already an asynchronous dispatch() running. New request will be ignored!"); return;
}
// check if somewhere wish to disable recovery temp. for this office session // This can be done immediately... must not been done asynchronous. if ((eNewJob & Job::DisableAutorecovery) == Job::DisableAutorecovery)
{ // it's important to set a flag internally, so AutoRecovery will be suppressed - even if it's requested.
m_eJob |= eNewJob;
implts_stopTimer();
implts_stopListening(); return;
}
// disable/enable AutoSave for this office session only // independent from the configuration entry. if ((eNewJob & Job::SetAutosaveState) == Job::SetAutosaveState)
{ bool bOn = lArgs.getUnpackedValueOrDefault(PROP_AUTOSAVE_STATE, true); if (bOn)
{ // don't enable AutoSave hardly ! // reload configuration to know the current state.
implts_readAutoSaveConfig();
g.clear();
implts_updateTimer(); // can it happen that might be the listener was stopped? .-) // make sure it runs always... even if AutoSave itself was disabled temporarily.
implts_startListening();
} else
{
implts_stopTimer();
m_eJob &= ~Job::AutoSave;
m_eTimerType = AutoRecovery::E_DONT_START_TIMER;
} return;
}
// in case a new dispatch overwrites a may ba active AutoSave session // we must restore this session later. see below ... bool bWasAutoSaveActive = ((eJob & Job::AutoSave) == Job::AutoSave); bool bWasUserAutoSaveActive =
((eJob & Job::UserAutoSave) == Job::UserAutoSave);
// On the other side it makes no sense to reactivate the AutoSave operation // if the new dispatch indicates a final decision... // E.g. an EmergencySave/SessionSave indicates the end of life of the current office session. // It makes no sense to reactivate an AutoSave then. // But a Recovery or SessionRestore should reactivate a may be already active AutoSave. bool bAllowAutoSaveReactivation = true;
// new document => put it into the internal list if (
(aEvent.EventName == EVENT_ON_NEW) ||
(aEvent.EventName == EVENT_ON_LOAD)
)
{
implts_registerDocument(xDocument);
} // document modified => set its modify state new (means modified against the original file!) elseif ( aEvent.EventName == EVENT_ON_MODIFYCHANGED )
{
implts_updateModifiedState(xDocument);
} /* at least one document starts saving process => Ourapplicationcodeisnotreadyformultiplesaverequests atthesametime.SowehavetosuppressourAutoSavefeature forthemoment,tillthisothersaverequestswillbefinished.
*/ elseif (
(aEvent.EventName == EVENT_ON_SAVE) ||
(aEvent.EventName == EVENT_ON_SAVEAS) ||
(aEvent.EventName == EVENT_ON_SAVETO)
)
{
implts_updateDocumentUsedForSavingState(xDocument, SAVE_IN_PROGRESS);
} // document saved => remove tmp. files - but hold config entries alive! elseif (
(aEvent.EventName == EVENT_ON_SAVEDONE) ||
(aEvent.EventName == EVENT_ON_SAVEASDONE)
)
{
SolarMutexGuard g;
implts_markDocumentAsSaved(xDocument);
implts_updateDocumentUsedForSavingState(xDocument, SAVE_FINISHED);
} /* document saved as copy => mark it as "non used by concurrent save operation". sowecantrytocreateabackupcopyifnexttimeAutoSaveisstartedtoo. Don'tremovetemp.filesorchangethemodifiedstateofthedocument! Itwasnotreallysavedtotheoriginalfile...
*/ elseif ( aEvent.EventName == EVENT_ON_SAVETODONE )
{
implts_updateDocumentUsedForSavingState(xDocument, SAVE_FINISHED);
} // If saving of a document failed by an error ... we have to save this document // by ourself next time AutoSave or EmergencySave is triggered. // But we can reset the state "used for other save requests". Otherwise // these documents will never be saved! elseif (
(aEvent.EventName == EVENT_ON_SAVEFAILED) ||
(aEvent.EventName == EVENT_ON_SAVEASFAILED) ||
(aEvent.EventName == EVENT_ON_SAVETOFAILED)
)
{
implts_updateDocumentUsedForSavingState(xDocument, SAVE_FINISHED);
} // document closed => remove temp. files and configuration entries elseif ( aEvent.EventName == EVENT_ON_UNLOAD )
{
implts_deregisterDocument(xDocument); // sal_True => stop listening for disposing() !
}
}
// Changes of the configuration must be ignored if AutoSave/Recovery was disabled for this // office session. That can happen if e.g. the command line arguments "--norestore" or "--headless" // was set. if ((m_eJob & Job::DisableAutorecovery) == Job::DisableAutorecovery) return;
for (i=0; i<c; ++i)
{
OUString sPath;
pChanges[i].Accessor >>= sPath;
// Note: This call stops the timer and starts it again. // But it checks the different timer states internally and // may be suppress the restart!
implts_updateTimer();
}
if (aEvent.Source == m_xNewDocBroadcaster)
{
m_xNewDocBroadcaster.clear(); return;
}
if (aEvent.Source == m_xRecoveryCFG)
{
m_xRecoveryCFG.clear(); return;
}
// dispose from one of our cached documents ? // Normally they should send a OnUnload message ... // But some stacktraces shows another possible use case .-)
css::uno::Reference< css::frame::XModel > xDocument(aEvent.Source, css::uno::UNO_QUERY); if (xDocument.is())
{
implts_deregisterDocument(xDocument, false); // sal_False => don't call removeEventListener() .. because it's not needed here return;
}
// throws a RuntimeException if an error occurs!
css::uno::Reference<css::container::XNameAccess> xCFG(
xConfigProvider->createInstanceWithArguments(
u"com.sun.star.configuration.ConfigurationAccess"_ustr,
comphelper::containerToSequence(lParams)),
css::uno::UNO_QUERY);
try
{
nMinSpaceDocSave = officecfg::Office::Recovery::AutoSave::MinSpaceDocSave::get();
nMinSpaceConfigSave = officecfg::Office::Recovery::AutoSave::MinSpaceConfigSave::get();
} catch(const css::uno::Exception&)
{ // These config keys are not sooooo important, that // we are interested on errors here really .-)
nMinSpaceDocSave = MIN_DISCSPACE_DOCSAVE;
nMinSpaceConfigSave = MIN_DISCSPACE_CONFIGSAVE;
}
if (pItems[i].startsWith(RECOVERY_ITEM_BASE_IDENTIFIER))
{
std::u16string_view sID = pItems[i].subView(RECOVERY_ITEM_BASE_IDENTIFIER.getLength());
aInfo.ID = o3tl::toInt32(sID); /* SAFE */ {
osl::MutexGuard g(cppu::WeakComponentImplHelperBase::rBHelper.rMutex); if (aInfo.ID > m_nIdPool)
{
m_nIdPool = aInfo.ID+1;
SAL_WARN_IF(m_nIdPool<0, "fwk.autorecovery", "AutoRecovery::implts_readConfig(): Overflow of IDPool detected!");
}
} /* SAFE */
} else
SAL_INFO("fwk.autorecovery", "AutoRecovery::implts_readConfig(): Who changed numbering of recovery items? Cache will be inconsistent then! I do not know, what will happen next time .-)");
void AutoRecovery::implts_specifyDefaultFilterAndExtension(AutoRecovery::TDocumentInfo& rInfo)
{ if (rInfo.AppModule.isEmpty())
{ throw css::uno::RuntimeException(
u"Can not find out the default filter and its extension, if no application module is known!"_ustr, static_cast< css::frame::XDispatch* >(this));
}
try
{ if (! xCFG.is())
{
implts_openConfig(); // open module config on demand and cache the update access
xCFG.set(officecfg::Setup::Office::Factories::get(),
css::uno::UNO_SET_THROW);
void AutoRecovery::implts_specifyAppModuleAndFactory(AutoRecovery::TDocumentInfo& rInfo)
{
ENSURE_OR_THROW2(
!rInfo.AppModule.isEmpty() || rInfo.Document.is(), "Can not find out the application module nor its factory URL, if no application module (or a suitable) document is known!",
*this );
void AutoRecovery::implts_collectActiveViewNames( AutoRecovery::TDocumentInfo& i_rInfo )
{
ENSURE_OR_THROW2( i_rInfo.Document.is(), "need at document, at the very least", *this );
i_rInfo.ViewNames.realloc(0);
// obtain list of controllers of this document
::std::vector< OUString > aViewNames; const Reference< XModel2 > xModel( i_rInfo.Document, UNO_QUERY ); if ( xModel.is() )
{ const Reference< css::container::XEnumeration > xEnumControllers( xModel->getControllers() ); while ( xEnumControllers->hasMoreElements() )
{ const Reference< XController2 > xController( xEnumControllers->nextElement(), UNO_QUERY );
OUString sViewName; if ( xController.is() )
sViewName = xController->getViewControllerName();
OSL_ENSURE( !sViewName.isEmpty(), "AutoRecovery::implts_collectActiveViewNames: (no XController2 ->) no view name -> no recovery of this view!" );
if ( !sViewName.isEmpty() )
aViewNames.push_back( sViewName );
}
}
// This list will be filled with every document for (auto & elem : m_lDocCache)
{
implts_collectActiveViewNames(elem);
implts_flushConfigItem(elem);
}
}
// remove if (bRemoveIt)
{ // Catch NoSuchElementException. // It's not a good idea inside multithreaded environments to call hasElement - removeElement. // DO IT! try
{
osl::File::remove(rInfo.OldTempURL);
osl::File::remove(rInfo.NewTempURL);
rInfo.OldTempURL.clear();
rInfo.NewTempURL.clear();
xModify->removeByName(sID);
} catch(const css::container::NoSuchElementException&)
{ return;
}
} else
{ // new/modify
css::uno::Reference< css::beans::XPropertySet > xSet; bool bNew = !xCheck->hasByName(sID); if (bNew)
{ if (!bAllowAdd) return; // no change made, just exit
if (bNew)
xModify->insertByName(sID, css::uno::Any(xSet));
}
} catch(const css::uno::RuntimeException&)
{ throw;
} catch(const css::uno::Exception&)
{ // ??? can it happen that a full disc let these set of operations fail too ???
}
sal_Int32 nRetry = RETRY_STORE_ON_FULL_DISC_FOREVER; do
{ try
{
batch->commit();
#ifdef TRIGGER_FULL_DISC_CHECK throw css::uno::Exception("trigger full disk check"); #else// TRIGGER_FULL_DISC_CHECK
nRetry = 0; #endif// TRIGGER_FULL_DISC_CHECK
} catch(const css::uno::Exception&)
{ // a) FULL DISC seems to be the problem behind => show error and retry it forever (e.g. retry=300) // b) unknown problem (may be locking problem) => reset RETRY value to more useful value(!) (e.g. retry=3) // c) unknown problem (may be locking problem) + 1..2 repeating operations => throw the original exception to force generation of a stacktrace !
if (! impl_enoughDiscSpace(nMinSpaceConfigSave))
AutoRecovery::impl_showFullDiscError(); elseif (nRetry > RETRY_STORE_ON_MIGHT_FULL_DISC_USEFULL)
nRetry = RETRY_STORE_ON_MIGHT_FULL_DISC_USEFULL; elseif (nRetry <= GIVE_UP_RETRY) throw; // force stacktrace to know if there exist might other reasons, why an AutoSave can fail !!!
void AutoRecovery::implts_stopListening()
{
css::uno::Reference< css::util::XChangesNotifier > xCFG;
css::uno::Reference< css::document::XDocumentEventBroadcaster > xGlobalEventBroadcaster; /* SAFE */ {
osl::MutexGuard g(cppu::WeakComponentImplHelperBase::rBHelper.rMutex); // Attention: Don't reset our internal members here too. // May be we must work with our configuration, but don't wish to be informed // about changes any longer. Needed e.g. during Job::EmergencySave!
xCFG.set (m_xRecoveryCFG , css::uno::UNO_QUERY);
xGlobalEventBroadcaster = m_xNewDocBroadcaster;
} /* SAFE */
if (xGlobalEventBroadcaster.is() && m_bListenForDocEvents)
{
xGlobalEventBroadcaster->removeDocumentEventListener(m_xNewDocBroadcasterListener);
m_bListenForDocEvents = false;
}
if (
(m_eJob == Job::NoJob ) || // TODO may be superfluous - E_DONT_START_TIMER should be used only
(m_eTimerType == AutoRecovery::E_DONT_START_TIMER)
) return;
if (m_eTimerType == AutoRecovery::E_NORMAL_AUTOSAVE_INTERVALL)
{ const sal_Int64 nConfiguredAutoSaveInterval
= officecfg::Office::Recovery::AutoSave::TimeIntervall::get()
* sal_Int64(60000); // [min] => 60.000 ms
nMilliSeconds = nConfiguredAutoSaveInterval;
// Calculate how soon the nearest dirty document's autosave time is; // store the shortest document autosave timeout as the next timer timeout. for (constauto& docInfo : m_lDocCache)
{ if (auto xDocRecovery2 = docInfo.Document.query<XDocumentRecovery2>())
{
sal_Int64 nDirtyDuration = xDocRecovery2->getModifiedStateDuration(); if (nDirtyDuration < 0) continue; if (nDirtyDuration > nConfiguredAutoSaveInterval)
nDirtyDuration = nConfiguredAutoSaveInterval; // nMilliSeconds will be 0
nMilliSeconds
= std::min(nMilliSeconds, nConfiguredAutoSaveInterval - nDirtyDuration);
}
}
} elseif (m_eTimerType == AutoRecovery::E_POLL_FOR_USER_IDLE)
{
nMilliSeconds = MIN_TIME_FOR_USER_IDLE;
} elseif (m_eTimerType == AutoRecovery::E_POLL_TILL_AUTOSAVE_IS_ALLOWED)
nMilliSeconds = 300; // there is a minimum time frame, where the user can lose some key input data!
if (!m_aTimer.IsActive()) return;
m_aTimer.Stop();
}
IMPL_LINK_NOARG(AutoRecovery, implts_timerExpired, Timer *, void)
{ try
{ // This method is called by using a pointer to us. // But we must be aware that we can be destroyed hardly // if our uno reference will be gone! // => Hold this object alive till this method finish its work.
css::uno::Reference< css::uno::XInterface > xSelfHold(static_cast< css::lang::XTypeProvider* >(this));
// Needed! Otherwise every reschedule request allow a new triggered timer event :-(
implts_stopTimer();
// The timer must be ignored if AutoSave/Recovery was disabled for this // office session. That can happen if e.g. the command line arguments "--norestore" or "--headless" // was set. But normally the timer was disabled if recovery was disabled ... // But so we are more "safe" .-) /* SAFE */ {
osl::MutexGuard g(cppu::WeakComponentImplHelperBase::rBHelper.rMutex); if ((m_eJob & Job::DisableAutorecovery) == Job::DisableAutorecovery) return;
} /* SAFE */
// check some "states", where it's not allowed (better: not a good idea) to // start an AutoSave. (e.g. if the user makes drag & drop ...) // Then we poll till this "disallowed" state is gone. bool bAutoSaveNotAllowed = Application::IsUICaptured(); if (bAutoSaveNotAllowed)
{ /* SAFE */ {
osl::MutexGuard g(cppu::WeakComponentImplHelperBase::rBHelper.rMutex);
m_eTimerType = AutoRecovery::E_POLL_TILL_AUTOSAVE_IS_ALLOWED;
} /* SAFE */
implts_updateTimer(); return;
}
// analyze timer type. // If we poll for an user idle period, may be we must // do nothing here and start the timer again. /* SAFE */ {
osl::ClearableMutexGuard g(cppu::WeakComponentImplHelperBase::rBHelper.rMutex);
if (m_eTimerType == AutoRecovery::E_POLL_FOR_USER_IDLE)
{ bool bUserIdle = Application::GetLastInputInterval() > MIN_TIME_FOR_USER_IDLE; if (!bUserIdle)
{
g.clear();
implts_updateTimer(); return;
}
}
// force save of all currently open documents // The called method returns an info, if and how this // timer must be restarted. constbool bIsAlreadyIdle(m_eTimerType == AutoRecovery::E_POLL_FOR_USER_IDLE);
AutoRecovery::ETimerType eSuggestedTimer
= implts_saveDocs(/*AllowUserIdleLoop=*/!bIsAlreadyIdle, /*RemoveLockFiles=*/false);
// If timer is not used for "short callbacks" (means polling // for special states) ... reset the handle state of all // cache items. Such handle state indicates, that a document // was already saved during the THIS(!) AutoSave session. // Of course NEXT AutoSave session must be started without // any "handle" state ... if (
(eSuggestedTimer == AutoRecovery::E_DONT_START_TIMER ) ||
(eSuggestedTimer == AutoRecovery::E_NORMAL_AUTOSAVE_INTERVALL)
)
{
implts_resetHandleStates();
}
void AutoRecovery::implts_registerDocument(const css::uno::Reference< css::frame::XModel3 > & xDocument)
{ // ignore corrupted events, where no document is given ... Runtime Error ?! if (!xDocument.is()) return;
// notification for already existing document ! // Can happen if events came in asynchronous on recovery time. // Then our cache was filled from the configuration ... but now we get some // asynchronous events from the global event broadcaster. We must be sure that // we don't add the same document more than once.
AutoRecovery::TDocumentList::iterator pIt = AutoRecovery::impl_searchDocument(m_lDocCache, xDocument); if (pIt != m_lDocCache.end())
{ // Normally nothing must be done for this "late" notification. // But may be the modified state was changed in between. // Check it...
implts_updateModifiedState(xDocument); return;
}
// check if this document must be ignored for recovery ! // Some use cases don't wish support for AutoSave/Recovery ... as e.g. OLE-Server / ActiveX Control etcpp. bool bNoAutoSave = lDescriptor.getUnpackedValueOrDefault(utl::MediaDescriptor::PROP_NOAUTOSAVE, false); if (bNoAutoSave) return;
// Check if doc is well known on the desktop. Otherwise ignore it! // Other frames mostly are used from external programs - e.g. the bean ...
css::uno::Reference< css::frame::XController > xController = xDocument->getCurrentController(); if (!xController.is()) return;
css::uno::Reference< css::frame::XFrame > xFrame = xController->getFrame(); if (!xFrame.is()) return;
css::uno::Reference< css::frame::XDesktop > xDesktop (xFrame->getCreator(), css::uno::UNO_QUERY); if (!xDesktop.is()) return;
// if the document doesn't support the XDocumentRecovery interface, we're not interested in it.
Reference< XDocumentRecovery > xDocRecovery( xDocument, UNO_QUERY ); if ( !xDocRecovery.is() ) return;
// get all needed information of this document // We need it to update our cache or to locate already existing elements there!
AutoRecovery::TDocumentInfo aNew;
aNew.Document = xDocument;
// TODO replace getLocation() with getURL() ... it's a workaround currently only!
css::uno::Reference< css::frame::XStorable > xDoc(aNew.Document, css::uno::UNO_QUERY_THROW);
aNew.OrgURL = xDoc->getLocation();
// classify the used application module, which is used by this document.
implts_specifyAppModuleAndFactory(aNew);
// Hack! Check for "illegal office documents"... as e.g. the Basic IDE // It's not really a full featured office document. It doesn't provide a URL, any filter, a factory URL etcpp. // TODO file bug to Basic IDE developers. They must remove the office document API from its service. if (
(aNew.OrgURL.isEmpty()) &&
(aNew.FactoryURL.isEmpty())
)
{
OSL_FAIL( "AutoRecovery::implts_registerDocument: this should not happen anymore!" ); // nowadays, the Basic IDE should already die on the "supports XDocumentRecovery" check. And no other known // document type fits in here ... return;
}
// By the way - get some information about the default format for saving! // and save an information about the real used filter by this document. // We save this document with DefaultFilter ... and load it with the RealFilter.
implts_specifyDefaultFilterAndExtension(aNew);
aNew.RealFilter = lDescriptor.getUnpackedValueOrDefault(utl::MediaDescriptor::PROP_FILTERNAME, OUString());
// Further we must know, if this document base on a template. // Then we must load it in a different way.
css::uno::Reference< css::document::XDocumentPropertiesSupplier > xSupplier(aNew.Document, css::uno::UNO_QUERY); if (xSupplier.is()) // optional interface!
{
css::uno::Reference< css::document::XDocumentProperties > xDocProps(xSupplier->getDocumentProperties(), css::uno::UNO_SET_THROW);
aNew.TemplateURL = xDocProps->getTemplateURL();
}
// create a new cache entry ... this document is not known.
++m_nIdPool;
aNew.ID = m_nIdPool;
SAL_WARN_IF(m_nIdPool<0, "fwk.autorecovery", "AutoRecovery::implts_registerDocument(): Overflow of ID pool detected.");
m_lDocCache.push_back(aNew);
// Don't register new documents here. (They are registered elsewhere when saved or modified...)
implts_flushConfigItem(aInfo, /*bRemoveIt=*/false, /*bAllowAdd=*/!aNew.OrgURL.isEmpty());
implts_startModifyListeningOnDoc(aInfo);
// Attention: Don't leave SAFE section, if you work with pIt! // Because it points directly into the m_lDocCache list ...
CacheLockGuard aCacheLock(this, cppu::WeakComponentImplHelperBase::rBHelper.rMutex, m_nDocCacheLock, LOCK_FOR_CACHE_USE);
AutoRecovery::TDocumentList::iterator pIt = AutoRecovery::impl_searchDocument(m_lDocCache, xDocument); if (pIt == m_lDocCache.end()) return; // unknown document => not a runtime error! Because we register only a few documents. see registration ...
aInfo = *pIt;
aCacheLock.unlock();
// Sometimes we close documents by ourself. // And these documents can't be deregistered. // Otherwise we lose our configuration data... but need it ! // see SessionSave ! if (aInfo.IgnoreClosing) return;
CacheLockGuard aCacheLock2(this, cppu::WeakComponentImplHelperBase::rBHelper.rMutex, m_nDocCacheLock, LOCK_FOR_CACHE_ADD_REMOVE);
pIt = AutoRecovery::impl_searchDocument(m_lDocCache, xDocument); if (pIt != m_lDocCache.end())
m_lDocCache.erase(pIt);
pIt = m_lDocCache.end(); // otherwise it's not specified what pIt means!
aCacheLock2.unlock();
} /* SAFE */
/* This method is called within disposing() of the document too. But there it's not a good idea to deregisterusaslistener.Furtheritmakesnosense-becausethebroadcasterdies. Sowesuppressderegistrationinsuchcase...
*/ if (bStopListening)
implts_stopModifyListeningOnDoc(aInfo);
implts_flushConfigItem(aInfo, true); // sal_True => remove it from config
}
AutoRecovery::TDocumentList::iterator pIt = AutoRecovery::impl_searchDocument(m_lDocCache, xDocument); if (pIt != m_lDocCache.end())
{ /* Now we know, that this document was modified again and must be saved next time. Butwedon'tneedthisinformationforeverye.g.keyinputoftheuser. Sowestoplisteninghere. Butifthedocumentwassavedastemp.filewestartlisteningforthiseventagain.
*/
implts_stopModifyListeningOnDoc(*pIt);
}
} /* SAFE */
}
void AutoRecovery::implts_updateModifiedState(const css::uno::Reference< css::frame::XModel >& xDocument)
{ // use true as fallback to get every document on EmergencySave/AutoRecovery! bool bModified = true;
css::uno::Reference< css::util::XModifiable > xModify(xDocument, css::uno::UNO_QUERY); if (xModify.is())
bModified = xModify->isModified();
/* Since the document has been saved, update its entry in the document *cache.Weessentiallyresetthestateofthedocumentfroman *autorecoveryperspective,updatingthingslikethefilename(which *wouldchangeinthecaseofa'Saveas'operation)andtheassociated
* backup file URL. */
aInfo.DocumentState = DocState::Unknown; // TODO replace getLocation() with getURL() ... it's a workaround currently only!
css::uno::Reference< css::frame::XStorable > xDoc(aInfo.Document, css::uno::UNO_QUERY);
aInfo.OrgURL = xDoc->getLocation();
/* Save off the backup file URLs and then clear them. NOTE - it is *importantthatweclearthem-otherwise,wecouldenterastate *wherepIt->OldTempURL==pIt->NewTempURLandourbackupalgorithm *inimplts_saveOneDocwillwritetothatURLandthendeletethefile
* at that URL (bug #96607) */
sRemoveURL1 = aInfo.OldTempURL;
sRemoveURL2 = aInfo.NewTempURL;
aInfo.OldTempURL.clear();
aInfo.NewTempURL.clear();
/* Currently the document is not closed in case of crash, sothelockfilemustberemovedexplicitly
*/ void lc_removeLockFile(AutoRecovery::TDocumentInfo const & rInfo)
{ #if !HAVE_FEATURE_MULTIUSER_ENVIRONMENT || HAVE_FEATURE_MACOSX_SANDBOX
(void) rInfo; #else if ( !rInfo.Document.is() ) return;
for (auto & info : m_lDocCache)
{ // WORKAROUND... Since the documents are not closed the lock file must be removed explicitly // it is not done on documents saving since shutdown can be cancelled
lc_removeLockFile( info );
// Prevent us from deregistration of these documents. // Because we close these documents by ourself (see XClosable below) ... // it's fact, that we reach our deregistration method. There we // must not(!) update our configuration ... Otherwise all // session data are lost !!!
info.IgnoreClosing = true;
// reset modified flag of these documents (ignoring the notification about it!) // Otherwise a message box is shown on closing these models.
implts_stopModifyListeningOnDoc(info);
// if the session save is still running the documents should not be thrown away, // actually that would be a bad sign, that means that the SessionManager tries // to kill the session before the saving is ready if ((m_eJob & Job::SessionSave) != Job::SessionSave)
{
css::uno::Reference< css::util::XModifiable > xModify(info.Document, css::uno::UNO_QUERY); if (xModify.is())
xModify->setModified(false);
// close the model.
css::uno::Reference< css::util::XCloseable > xClose(info.Document, css::uno::UNO_QUERY); if (xClose.is())
{ try
{
xClose->close(false);
} catch(const css::uno::Exception&)
{ // At least it's only a try to close these documents before anybody else it does. // So it seems to be possible to ignore any error here .-)
}
// Set the default timer action for our call. // Default = NORMAL_AUTOSAVE // We return a suggestion for an active timer only. // It will be ignored if the timer was disabled by the user ... // Further this state can be set to USER_IDLE only later in this method. // It's not allowed to reset such state then. Because we must know, if // there exists POSTPONED documents. see below ...
AutoRecovery::ETimerType eTimer = AutoRecovery::E_NORMAL_AUTOSAVE_INTERVALL;
// This list will be filled with every document // which should be saved as last one. E.g. if it was used // already for a UI save operation => crashed ... and // now we try to save it again ... which can fail again ( of course .-) ).
::std::vector< AutoRecovery::TDocumentList::iterator > lDangerousDocs;
// WORKAROUND... Since the documents are not closed the lock file must be removed explicitly if ( bRemoveLockFiles )
lc_removeLockFile( aInfo );
// WORKAROUND ... see comment of this method if (lc_checkIfSaveForbiddenByArguments(aInfo)) continue;
// already auto saved during this session :-) // This state must be reset for all documents // if timer is started with normal AutoSaveTimerIntervall! if ((aInfo.DocumentState & DocState::Handled) == DocState::Handled) continue;
// don't allow implts_deregisterDocument to remove from RecoveryList during shutdown jobs if (m_eJob & (Job::EmergencySave | Job::SessionSave))
aInfo.IgnoreClosing = true;
// Not modified documents are not saved. // We save information about the URL only!
Reference< XDocumentRecovery > xDocRecover( aInfo.Document, UNO_QUERY_THROW ); if ( !xDocRecover->wasModifiedSinceLastSave() )
{
aInfo.DocumentState |= DocState::Handled;
*pIt = aInfo; continue;
}
// If the document became modified not too long ago, don't autosave it yet. if (bAllowUserIdleLoop)
{ if (auto xDocRecovery2 = xDocRecover.query<XDocumentRecovery2>())
{ const sal_Int64 nDirtyDuration = xDocRecovery2->getModifiedStateDuration(); // Round up to second - if this document is almost ready for autosave, do it now. if (nDirtyDuration + 999 < nConfiguredAutoSaveInterval)
{
aInfo.DocumentState |= DocState::Handled; continue;
}
}
}
// check if this document is still used by a concurrent save operation // e.g. if the user tried to save via UI. // Handle it in the following way: // i) For an AutoSave ... ignore this document! It will be saved and next time we will (hopefully) // get a notification about the state of this operation. // And if a document was saved by the user we can remove our temp. file. But that will be done inside // our callback for SaveDone notification. // ii) For a CrashSave ... add it to the list of dangerous documents and // save it after all other documents was saved successfully. That decrease // the chance for a crash inside a crash. // On the other side it's not necessary for documents, which are not modified. // They can be handled normally - means we patch the corresponding configuration entry only. // iii) For a SessionSave ... ignore it! There is no time to wait for this save operation. // Because the WindowManager will kill the process if it doesn't react immediately. // On the other side we can't risk a concurrent save request ... because we know // that it will produce a crash.
// Attention: Because eJob is used as a flag field, you have to check for the worst case first. // E.g. a CrashSave can overwrite an AutoSave. So you have to check for a CrashSave before an AutoSave! if (aInfo.UsedForSaving)
{ if ((eJob & Job::EmergencySave) == Job::EmergencySave)
{
lDangerousDocs.push_back(pIt); continue;
} else if ((eJob & Job::SessionSave) == Job::SessionSave)
{ continue;
} else if ((eJob & Job::AutoSave) == Job::AutoSave)
{
eTimer = AutoRecovery::E_POLL_TILL_AUTOSAVE_IS_ALLOWED;
aInfo.DocumentState |= DocState::Postponed; continue;
}
}
// a) Document was not postponed => wait for user idle if not urgent // b) Document was postponed => save it (because user idle/call_back was checked already) if (!(aInfo.DocumentState & DocState::Postponed))
{
aInfo.DocumentState |= DocState::Postponed;
*pIt = aInfo; // postponed documents will be saved if this method is called again! // That can be done by an outside started timer => E_POLL_FOR_USER_IDLE (if normal AutoSave is active) // or it must be done directly without starting any timer => E_CALL_ME_BACK (if Emergency- or SessionSave is active and must be finished ASAP!) if (!bAllowUserIdleLoop)
eTimer = AutoRecovery::E_CALL_ME_BACK; else
eTimer = AutoRecovery::E_POLL_FOR_USER_IDLE; continue;
}
// } /* SAFE */
g.clear(); // changing of aInfo and flushing it is done inside implts_saveOneDoc!
implts_saveOneDoc(sBackupPath, aInfo, xExternalProgress);
implts_informListener(eJob, AutoRecovery::implst_createFeatureStateEvent(eJob, OPERATION_UPDATE, &aInfo));
g.reset(); // /* SAFE */ {
*pIt = std::move(aInfo);
}
// Did we have some "dangerous candidates" ? // Try to save it ... but may be it will fail ! for (autoconst& dangerousDoc : lDangerousDocs)
{
pIt = dangerousDoc;
AutoRecovery::TDocumentInfo aInfo = *pIt;
// } /* SAFE */
g.clear(); // changing of aInfo and flushing it is done inside implts_saveOneDoc!
implts_saveOneDoc(sBackupPath, aInfo, xExternalProgress);
implts_informListener(eJob, AutoRecovery::implst_createFeatureStateEvent(eJob, OPERATION_UPDATE, &aInfo));
g.reset(); // /* SAFE */ {
*pIt = std::move(aInfo);
}
} /* SAFE */
return eTimer;
}
void AutoRecovery::implts_saveOneDoc(const OUString& sBackupPath ,
AutoRecovery::TDocumentInfo& rInfo , const css::uno::Reference< css::task::XStatusIndicator >& xExternalProgress)
{ // no document? => can occur if we loaded our configuration with files, // which couldn't be recovered successfully. In such case we have all needed information // excepting the real document instance!
// TODO: search right place, where such "dead files" can be removed from the configuration! if (!rInfo.Document.is()) return;
// if the document was loaded with a password, it should be // stored with password
utl::MediaDescriptor lNewArgs;
css::uno::Sequence< css::beans::NamedValue > aEncryptionData =
lOldArgs.getUnpackedValueOrDefault(utl::MediaDescriptor::PROP_ENCRYPTIONDATA,
css::uno::Sequence< css::beans::NamedValue >()); if (aEncryptionData.hasElements())
lNewArgs[utl::MediaDescriptor::PROP_ENCRYPTIONDATA] <<= aEncryptionData;
// Further it must be saved using the default file format of that application. // Otherwise we will some data lost. if (!rInfo.DefaultFilter.isEmpty())
lNewArgs[utl::MediaDescriptor::PROP_FILTERNAME] <<= rInfo.DefaultFilter;
// prepare frame/document/mediadescriptor in a way, that it uses OUR progress .-) if (xExternalProgress.is())
lNewArgs[utl::MediaDescriptor::PROP_STATUSINDICATOR] <<= xExternalProgress;
impl_establishProgress(rInfo, lNewArgs, css::uno::Reference< css::frame::XFrame >());
// #i66598# use special handling of property "DocumentBaseURL" (it must be an empty string!) // for make hyperlinks working
lNewArgs[utl::MediaDescriptor::PROP_DOCUMENTBASEURL] <<= OUString();
// try to save this document as a new temp file every time. // Mark AutoSave state as "INCOMPLETE" if it failed. // Because the last temp file is too old and does not include all changes.
Reference< XDocumentRecovery > xDocRecover(rInfo.Document, css::uno::UNO_QUERY_THROW);
// save the state about "trying to save" // ... we need it for recovery if e.g. a crash occurs inside next line!
rInfo.DocumentState |= DocState::TrySave; // just update existing info: don't add any recovery record until recovery file created.
implts_flushConfigItem(rInfo, /*bRemoveIt=*/false, /*bAllowAdd=*/false);
// If userautosave is enabled, first try to save the original file. // Note that we must do it *before* calling storeToRecoveryFile, so in case of failure here // we won't remain with the modified flag set to true, even though the autorecovery save succeeded. constbool bEmergencySave(m_eJob & Job::EmergencySave); bool bUserAutoSaved = false; try
{ // We must check here for an empty URL to avoid a "This operation is not supported on this operating system." // message during autosave. if (!bEmergencySave && m_eJob & Job::UserAutoSave && !rInfo.OrgURL.isEmpty())
{
Reference< XStorable > xDocSave(rInfo.Document, css::uno::UNO_QUERY_THROW);
xDocSave->store();
bUserAutoSaved = true;
}
} catch(const css::uno::Exception&)
{
}
// DocState::Modified status cannot be trusted to be accurate, but at least attempt to be so, // since this rInfo will eventually get assigned to m_lDocCache as the authoritative status. const Reference<css::util::XModifiable> xModify(rInfo.Document, UNO_QUERY); constbool bModified = xModify.is() && xModify->isModified(); if (bModified)
rInfo.DocumentState |= DocState::Modified; elseif (xModify.is())
rInfo.DocumentState &= ~DocState::Modified;
// If it is no longer modified, it is the same as on disk, and can be removed from RecoveryList. constbool bRemoveIt
= xModify.is() && !xModify->isModified() && bUserAutoSaved && !(m_eJob & Job::SessionSave);
sal_Int32 nRetry = RETRY_STORE_ON_FULL_DISC_FOREVER; bool bError = false; do
{ try
{ // skip recovery if it was already saved in-place. if (!bRemoveIt)
xDocRecover->storeToRecoveryFile(rInfo.NewTempURL,
lNewArgs.getAsConstPropertyValueList());
#ifdef TRIGGER_FULL_DISC_CHECK throw css::uno::Exception("trigger full disk check"); #else// TRIGGER_FULL_DISC_CHECK
// skip saving XLSX with protected sheets, if their passwords haven't supported yet if ( rException.Message.startsWith("SfxBaseModel::impl_store") )
{ const css::task::ErrorCodeIOException& pErrorCodeIOException = static_cast<const css::task::ErrorCodeIOException&>(rException); if ( static_cast<ErrCode>(pErrorCodeIOException.ErrCode) == ERRCODE_SFX_WRONGPASSWORD )
{ // stop and remove the bad temporary file, instead of filling the disk with them
bError = false; break;
}
}
// a) FULL DISC seems to be the problem behind => show error and retry it forever (e.g. retry=300) // b) unknown problem (may be locking problem) => reset RETRY value to more useful value(!) (e.g. retry=3) // c) unknown problem (may be locking problem) + 1..2 repeating operations => throw the original exception to force generation of a stacktrace !
if (! impl_enoughDiscSpace(nMinSpaceDocSave))
AutoRecovery::impl_showFullDiscError(); elseif (nRetry > RETRY_STORE_ON_MIGHT_FULL_DISC_USEFULL)
nRetry = RETRY_STORE_ON_MIGHT_FULL_DISC_USEFULL; elseif (nRetry <= GIVE_UP_RETRY)
{ // delete the empty file created by implts_generateNewTempURL if (tools::isEmptyFileUrl(rInfo.NewTempURL))
osl::File::remove(rInfo.NewTempURL);
throw; // force stacktrace to know if there exist might other reasons, why an AutoSave can fail !!!
}
--nRetry;
}
} while(nRetry>0);
if (! bError)
{ // safe the state about success // ... you know the reason: to know it on recovery time if next line crash .-)
rInfo.DocumentState &= ~DocState::TrySave;
rInfo.DocumentState |= DocState::Handled;
rInfo.DocumentState |= DocState::Succeeded;
} else
{ // safe the state about error ...
rInfo.NewTempURL.clear();
rInfo.DocumentState &= ~DocState::TrySave;
rInfo.DocumentState |= DocState::Handled;
rInfo.DocumentState |= DocState::Incomplete;
}
// make sure the progress is not referred any longer
impl_forgetProgress(rInfo, lNewArgs, css::uno::Reference< css::frame::XFrame >());
// try to remove the old temp file. // Ignore any error here. We have a new temp file, which is up to date. // The only thing is: we fill the disk with temp files, if we can't remove old ones :-)
OUString sRemoveFile = rInfo.OldTempURL;
rInfo.OldTempURL = rInfo.NewTempURL;
rInfo.NewTempURL.clear();
// If it is modified, a recovery file has just been created, so add to RecoveryList.
implts_flushConfigItem(rInfo, /*bRemoveIt=*/false, /*bAllowAdd=*/bModified);
// We must know if the user modifies the document again ...
implts_startModifyListeningOnDoc(rInfo);
Job eJob = m_eJob; for (auto & info : m_lDocCache)
{ // Such documents are already loaded by the last loop. // Don't check DocState::Succeeded here! It may be the final state of an AutoSave // operation before!!! if ((info.DocumentState & DocState::Handled) == DocState::Handled) continue;
// a1,b1,c1,d2,e2,f2) if ((info.DocumentState & DocState::Damaged) == DocState::Damaged)
{ // don't forget to inform listener! May be this document was // damaged on last saving time ... // Then our listener need this notification. // If it was damaged during last "try to open" ... // it will be notified more than once. SH.. HAPPENS ... // } /* SAFE */
g.clear();
implts_informListener(eJob,
AutoRecovery::implst_createFeatureStateEvent(eJob, OPERATION_UPDATE, &info));
g.reset(); // /* SAFE */ { continue;
}
utl::MediaDescriptor lDescriptor;
// it's a UI feature - so the "USER" itself must be set as referrer
lDescriptor[utl::MediaDescriptor::PROP_REFERRER] <<= OUString(REFERRER_USER);
lDescriptor[utl::MediaDescriptor::PROP_SALVAGEDFILE] <<= OUString();
// recovered documents are loaded hidden, and shown all at once, later
lDescriptor[utl::MediaDescriptor::PROP_HIDDEN] <<= true;
if (aParams.m_xProgress.is())
lDescriptor[utl::MediaDescriptor::PROP_STATUSINDICATOR] <<= aParams.m_xProgress;
bool bBackupWasTried = (
((info.DocumentState & DocState::TryLoadBackup ) == DocState::TryLoadBackup) || // temp. state!
((info.DocumentState & DocState::Incomplete ) == DocState::Incomplete ) // transport DocState::TryLoadBackup from last loop to this new one!
); bool bOriginalWasTried = ((info.DocumentState & DocState::TryLoadOriginal) == DocState::TryLoadOriginal);
if (bBackupWasTried)
{ if (!bOriginalWasTried)
{
info.DocumentState |= DocState::Incomplete; // try original URL ... ! don't continue with next item here ...
} else
{
info.DocumentState |= DocState::Damaged; continue;
}
}
// A "Salvaged" item must exists every time. The core can make something special then for recovery. // Of course it should be the real file name of the original file, in case we load the temp. backup here.
OUString sURL; if (!sLoadBackupURL.isEmpty())
{
sURL = sLoadBackupURL;
info.DocumentState |= DocState::TryLoadBackup;
lDescriptor[utl::MediaDescriptor::PROP_SALVAGEDFILE] <<= sLoadOriginalURL;
} elseif (!sLoadOriginalURL.isEmpty())
{
sURL = sLoadOriginalURL;
info.DocumentState |= DocState::TryLoadOriginal;
} else continue; // TODO ERROR!
// /* SAFE */ { // Needed for next loop!
g.reset(); continue;
}
if (!info.RealFilter.isEmpty())
{
utl::MediaDescriptor lPatchDescriptor(info.Document->getArgs());
lPatchDescriptor[utl::MediaDescriptor::PROP_FILTERNAME] <<= info.RealFilter;
info.Document->attachResource(info.Document->getURL(), lPatchDescriptor.getAsConstPropertyValueList()); // do *not* use sURL here. In case this points to the recovery file, it has already been passed // to recoverFromFile. Also, passing it here is logically wrong, as attachResource is intended // to take the logical file URL.
}
/* Normally we listen as XModifyListener on a document to know if a document was changed sinceourlastAutoSave.Andwederegisterusincaseweknowthisstate. Butdirectlyafteronedocumentasrecovered...wemuststartlistening. Otherwisethefirst"modify"doesn'treachus.BecauseweourselfcalledsetModified() onthedocumentviaAPI.Andcurrentlywedon'tlistenforanyevents(notattheGlobalEventBroadcaster noratanydocument!).
*/
implts_startModifyListeningOnDoc(info);
// /* SAFE */ { // Needed for next loop. Don't unlock it again!
g.reset();
}
::std::vector< Reference< XComponent > > aCleanup; try
{ // create a new document of the desired type
Reference< XModel2 > xModel(m_xContext->getServiceManager()->createInstanceWithContext(
rInfo.FactoryService, m_xContext), UNO_QUERY_THROW);
aCleanup.emplace_back(xModel.get() );
// put the filter name into the descriptor - we're not going to involve any type detection, so // the document might be lost without the FilterName property if ( (rInfo.DocumentState & DocState::TryLoadOriginal) == DocState::TryLoadOriginal)
lDescriptor[ utl::MediaDescriptor::PROP_FILTERNAME ] <<= rInfo.RealFilter; else
lDescriptor[ utl::MediaDescriptor::PROP_FILTERNAME ] <<= rInfo.DefaultFilter;
if ( sURL == rInfo.FactoryURL )
{ // if the document was a new, unmodified document, then there's nothing to recover, just to init
ENSURE_OR_THROW( ( rInfo.DocumentState & DocState::Modified ) == DocState(0), "unexpected document state" );
Reference< XLoadable > xModelLoad( xModel, UNO_QUERY_THROW );
xModelLoad->initNew();
// TODO: remove load-process specific arguments from the descriptor, e.g. the status indicator
xModel->attachResource( sURL, lDescriptor.getAsConstPropertyValueList() );
} elseif (!utl::UCBContentHelper::Exists(sURL)) throw css::uno::Exception(); else
{
OUString sFilterName;
lDescriptor[utl::MediaDescriptor::PROP_FILTERNAME] >>= sFilterName; if (!sFilterName.isEmpty()
&& ( sFilterName == "Calc MS Excel 2007 XML"
|| sFilterName == "Impress MS PowerPoint 2007 XML"
|| sFilterName == "MS Word 2007 XML")) // TODO: Probably need to check other affected formats + templates?
{ // tdf#129096: in case of recovery of password protected OOXML document it is done not // the same way as ordinal loading. Inside XDocumentRecovery::recoverFromFile // there is a call to XFilter::filter which has constant media descriptor and thus // all encryption data used in document is lost. To avoid this try to walkaround // with explicit call to FormatDetector. It will try to load document, prompt for password // and store this info in media descriptor we will use for recoverFromFile call.
Reference< css::document::XExtendedFilterDetection > xDetection(
m_xContext->getServiceManager()->createInstanceWithContext(
u"com.sun.star.comp.oox.FormatDetector"_ustr, m_xContext),
UNO_QUERY_THROW);
lDescriptor[utl::MediaDescriptor::PROP_URL] <<= sURL;
Sequence< css::beans::PropertyValue > aDescriptorSeq = lDescriptor.getAsConstPropertyValueList();
OUString sType = xDetection->detect(aDescriptorSeq);
OUString sNewFilterName;
lDescriptor[utl::MediaDescriptor::PROP_FILTERNAME] >>= sNewFilterName; if (!sType.isEmpty() && sNewFilterName == sFilterName)
{ // Filter detection was okay, update media descriptor with one received from FilterDetect
lDescriptor = aDescriptorSeq;
}
}
// let it recover itself
Reference< XDocumentRecovery > xDocRecover( xModel, UNO_QUERY_THROW );
xDocRecover->recoverFromFile(
sURL,
lDescriptor.getUnpackedValueOrDefault( utl::MediaDescriptor::PROP_SALVAGEDFILE, OUString() ),
lDescriptor.getAsConstPropertyValueList()
);
// No attachResource needed here. By definition (of XDocumentRecovery), the implementation is responsible // for completely initializing the model, which includes attachResource (or equivalent), if required.
}
// re-create all the views
::std::vector< OUString > aViewsToRestore( std::cbegin(rInfo.ViewNames), std::cend(rInfo.ViewNames) ); // if we don't have views for whatever reason, then create a default-view, at least if ( aViewsToRestore.empty() )
aViewsToRestore.emplace_back( );
void AutoRecovery::implts_generateNewTempURL(const OUString& sBackupPath ,
utl::MediaDescriptor& /*rMediaDescriptor*/,
AutoRecovery::TDocumentInfo& rInfo )
{ // specify URL for saving (which points to a temp file inside backup directory) // and define a unique name, so we can locate it later. // This unique name must solve an optimization problem too! // In case we are asked to save unmodified documents too - and one of them // is an empty one (because it was new created using e.g. a URL private:factory/...) // we should not save it really. Then we put the information about such "empty document" // into the configuration and don't create any recovery file on disk. // We use the title of the document to make it unique.
OUStringBuffer sUniqueName; if (!rInfo.OrgURL.isEmpty())
{
css::uno::Reference< css::util::XURLTransformer > xParser(css::util::URLTransformer::create(m_xContext));
css::util::URL aURL;
aURL.Complete = rInfo.OrgURL;
xParser->parseStrict(aURL);
sUniqueName.append(aURL.Name);
} elseif (!rInfo.FactoryURL.isEmpty())
sUniqueName.append("untitled");
sUniqueName.append("_");
// TODO: Must we strip some illegal signs - if we use the title?
OUString AutoRecovery::implst_getJobDescription(Job eJob)
{ // describe the current running operation
OUStringBuffer sFeature(256);
sFeature.append(CMD_PROTOCOL);
// Attention: Because "eJob" is used as a flag field the order of checking these // flags is important. We must prefer job with higher priorities! // E.g. EmergencySave has an higher prio then AutoSave ... // On the other side there exist a well defined order between two different jobs. // e.g. PrepareEmergencySave must be done before EmergencySave is started of course.
void AutoRecovery::implts_prepareEmergencySave()
{ // Be sure to know all open documents really .-)
implts_verifyCacheAgainstDesktopDocumentList();
// hide all docs, so the user can't disturb our emergency save .-)
implts_changeAllDocVisibility(false);
}
void AutoRecovery::implts_doEmergencySave(const DispatchParams& aParams)
{ // Write a hint "we crashed" into the configuration, so // the error report tool is started too in case no recovery // documents exists and was saved.
// for all docs, store their current view/names in the configuration
implts_persistAllActiveViewNames();
// The called method for saving documents runs // during normal AutoSave more than once. Because // it postpone active documents and save it later. // That is normally done by recalling it from a timer. // Here we must do it immediately! // Of course this method returns the right state - // because it knows, that we are running in EMERGENCY SAVE mode .-)
boolconst bAllowUserIdleLoop = false; // not allowed to change that .-)
AutoRecovery::ETimerType eSuggestedTimer = AutoRecovery::E_DONT_START_TIMER; do
{
eSuggestedTimer = implts_saveDocs(bAllowUserIdleLoop, true, &aParams);
} while(eSuggestedTimer == AutoRecovery::E_CALL_ME_BACK);
// reset the handle state of all // cache items. Such handle state indicates, that a document // was already saved during the THIS(!) EmergencySave session. // Of course following recovery session must be started without // any "handle" state ...
implts_resetHandleStates();
// flush config cached back to disc.
impl_flushALLConfigChanges();
// try to make sure next time office will be started user won't be // notified about any other might be running office instance // remove ".lock" file from disc !
AutoRecovery::st_impl_removeLockFile();
}
// reset the handle state of all // cache items. Such handle state indicates, that a document // was already saved during the THIS(!) Recovery session. // Of course a may be following EmergencySave session must be started without // any "handle" state...
implts_resetHandleStates();
// Reset the configuration hint "we were crashed"!
std::shared_ptr<comphelper::ConfigurationChanges> batch(
comphelper::ConfigurationChanges::create());
officecfg::Office::Recovery::RecoveryInfo::Crashed::set(false, batch);
batch->commit();
}
// Be sure to know all open documents really .-)
implts_verifyCacheAgainstDesktopDocumentList();
// for all docs, store their current view/names in the configuration
implts_persistAllActiveViewNames();
// The called method for saving documents runs // during normal AutoSave more than once. Because // it postpone active documents and save it later. // That is normally done by recalling it from a timer. // Here we must do it immediately! // Of course this method returns the right state - // because it knows, that we are running in SESSION SAVE mode .-)
boolconst bAllowUserIdleLoop = false; // not allowed to change that .-)
AutoRecovery::ETimerType eSuggestedTimer = AutoRecovery::E_DONT_START_TIMER; do
{ // do not remove lock files of the documents, it will be done on session quit
eSuggestedTimer = implts_saveDocs(bAllowUserIdleLoop, false, &aParams);
} while(eSuggestedTimer == AutoRecovery::E_CALL_ME_BACK);
// reset the handle state of all // cache items. Such handle state indicates, that a document // was already saved during the THIS(!) save session. // Of course following restore session must be started without // any "handle" state ...
implts_resetHandleStates();
// flush config cached back to disc.
impl_flushALLConfigChanges();
}
// try to make sure next time office will be started user won't be // notified about any other might be running office instance // remove ".lock" file from disc! // it is done as a first action for session save since Gnome sessions // do not provide enough time for shutdown, and the dialog looks to be // confusing for the user
AutoRecovery::st_impl_removeLockFile();
// reset all modified documents, so the don't show any UI on closing ... // and close all documents, so we can shutdown the OS!
implts_prepareSessionShutdown();
// Write a hint for "stored session data" into the configuration, so // the on next startup we know what's happen last time
std::shared_ptr<comphelper::ConfigurationChanges> batch(
comphelper::ConfigurationChanges::create());
officecfg::Office::Recovery::RecoveryInfo::SessionData::set(true, batch);
batch->commit();
// flush config cached back to disc.
impl_flushALLConfigChanges();
}
// reset the handle state of all // cache items. Such handle state indicates, that a document // was already saved during the THIS(!) Restore session. // Of course a may be following save session must be started without // any "handle" state ...
implts_resetHandleStates();
// make all opened documents visible
implts_changeAllDocVisibility(true);
// Reset the configuration hint for "session save"!
SAL_INFO("fwk.autorecovery", "... reset config key 'SessionData'");
std::shared_ptr<comphelper::ConfigurationChanges> batch(
comphelper::ConfigurationChanges::create());
officecfg::Office::Recovery::RecoveryInfo::SessionData::set(false, batch);
batch->commit();
for (autoconst& info : m_lDocCache)
{ if (info.ID != aParams.m_nWorkingEntryID) continue;
OUString sSourceURL; // Prefer temp file. It contains the changes against the original document! if (!info.OldTempURL.isEmpty())
sSourceURL = info.OldTempURL; elseif (!info.NewTempURL.isEmpty())
sSourceURL = info.NewTempURL; elseif (!info.OrgURL.isEmpty())
sSourceURL = info.OrgURL; else continue; // nothing real to save! An unmodified but new created document.
// TODO: Check eResult and react for errors (InteractionHandler!?) // Currently we ignore it ... // DON'T UPDATE THE CACHE OR REMOVE ANY TEMP. FILES FROM DISK. // That has to be forced from outside explicitly. // See implts_cleanUpWorkingEntry() for further details.
}
}
AutoRecovery::TDocumentList::iterator pIt = std::find_if(m_lDocCache.begin(), m_lDocCache.end(),
[&aParams](const AutoRecovery::TDocumentInfo& rInfo) { return rInfo.ID == aParams.m_nWorkingEntryID; }); if (pIt != m_lDocCache.end())
{
AutoRecovery::TDocumentInfo& rInfo = *pIt;
implts_flushConfigItem(rInfo, true); // sal_True => remove it from xml config!
m_lDocCache.erase(pIt);
}
}
AutoRecovery::EFailureSafeResult AutoRecovery::implts_copyFile(const OUString& sSource , const OUString& sTargetPath, const OUString& sTargetName)
{ // create content for the parent folder and call transfer on that content with the source content // and the destination file name as parameters
// exists session data ... => then we can't say, that these // data are valid for recovery. So we have to return sal_False then! if (bSessionData)
bRecoveryData = false;
aValue <<= bRecoveryData;
} break;
case AUTORECOVERY_PROPHANDLE_CRASHED :
aValue <<= officecfg::Office::Recovery::RecoveryInfo::Crashed::get(); break;
case AUTORECOVERY_PROPHANDLE_EXISTS_SESSIONDATA :
aValue <<= officecfg::Office::Recovery::RecoveryInfo::SessionData::get(); break;
}
}
sal_Int32 i = 0;
sal_Int32 c = xContainer->getCount();
for (i=0; i<c; ++i)
{
css::uno::Reference< css::frame::XFrame > xFrame; try
{
xContainer->getByIndex(i) >>= xFrame; if (!xFrame.is()) continue;
} // can happen in multithreaded environments, that frames was removed from the container during this loop runs! // Ignore it. catch(const css::lang::IndexOutOfBoundsException&)
{ continue;
}
// We are interested on visible documents only. // Note: It's n optional interface .-(
css::uno::Reference< css::awt::XWindow2 > xVisibleCheck(
xFrame->getContainerWindow(),
css::uno::UNO_QUERY); if (
(!xVisibleCheck.is() ) ||
(!xVisibleCheck->isVisible())
)
{ continue;
}
// extract the model from the frame. // Ignore "view only" frames, which does not have a model.
css::uno::Reference< css::frame::XController > xController;
css::uno::Reference< css::frame::XModel3 > xModel;
xController = xFrame->getController(); if (xController.is())
xModel.set( xController->getModel(), UNO_QUERY_THROW ); if (!xModel.is()) continue;
// insert model into cache ... // If the model is already well known inside cache // it's information set will be updated by asking the // model again for its new states.
implts_registerDocument(xModel);
}
} catch(const css::uno::RuntimeException&)
{ throw;
} catch(const css::uno::Exception&)
{
}
bool AutoRecovery::impl_enoughDiscSpace(sal_Int32 nRequiredSpace)
{ #ifdef SIMULATE_FULL_DISC return sal_False; #else// SIMULATE_FULL_DISC // In case an error occurs and we are not able to retrieve the needed information // it's better to "disable" the feature ShowErrorOnFullDisc ! // Otherwise we start a confusing process of error handling ...
// static void AutoRecovery::impl_establishProgress(const AutoRecovery::TDocumentInfo& rInfo ,
utl::MediaDescriptor& rArgs , const css::uno::Reference< css::frame::XFrame >& xNewFrame)
{ // external well known frame must be preferred (because it was created by ourself // for loading documents into this frame)! // But if no frame exists... we can try to locate it using any frame bound to the provided // document. Of course we must live without any frame in case the document does not exists at this // point. But this state should not occur. In such case xNewFrame should be valid ... hopefully .-)
css::uno::Reference< css::frame::XFrame > xFrame = xNewFrame; if (
(!xFrame.is() ) &&
(rInfo.Document.is())
)
{
css::uno::Reference< css::frame::XController > xController = rInfo.Document->getCurrentController(); if (xController.is())
xFrame = xController->getFrame();
}
// Any outside progress must be used ... // Only if there is no progress, we can create our own one.
css::uno::Reference< css::task::XStatusIndicator > xInternalProgress;
css::uno::Reference< css::task::XStatusIndicator > xExternalProgress = rArgs.getUnpackedValueOrDefault(
utl::MediaDescriptor::PROP_STATUSINDICATOR,
css::uno::Reference< css::task::XStatusIndicator >() );
// Normally a progress is set from outside (e.g. by the CrashSave/Recovery dialog, which uses our dispatch API). // But for a normal auto save we don't have such "external progress"... because this function is triggered by our own timer then. // In such case we must create our own progress ! if (
(! xExternalProgress.is()) &&
(xFrame.is() )
)
{
css::uno::Reference< css::task::XStatusIndicatorFactory > xProgressFactory(xFrame, css::uno::UNO_QUERY); if (xProgressFactory.is())
xInternalProgress = xProgressFactory->createStatusIndicator();
}
// HACK // An external provided progress (most given by the CrashSave/Recovery dialog) // must be preferred. But we know that some application filters query its own progress instance // at the frame method Frame::createStatusIndicator(). // So we use a two step mechanism: // 1) we set the progress inside the MediaDescriptor, which will be provided to the filter // 2) and we set a special Frame property, which overwrites the normal behaviour of Frame::createStatusIndicator .-) // But we suppress 2) in case we uses an internal progress. Because then it doesn't matter // if our applications make it wrong. In such case the internal progress resists at the same frame // and there is no need to forward progress activities to e.g. an outside dialog .-) if (
(xExternalProgress.is()) &&
(xFrame.is() )
)
{
css::uno::Reference< css::beans::XPropertySet > xFrameProps(xFrame, css::uno::UNO_QUERY); if (xFrameProps.is())
xFrameProps->setPropertyValue(FRAME_PROPNAME_ASCII_INDICATORINTERCEPTION, css::uno::Any(xExternalProgress));
}
// But inside the MediaDescriptor we must set our own create progress ... // in case there is not already another progress set.
rArgs.createItemIfMissing(utl::MediaDescriptor::PROP_STATUSINDICATOR, xInternalProgress);
}
// static void AutoRecovery::impl_forgetProgress(const AutoRecovery::TDocumentInfo& rInfo ,
utl::MediaDescriptor& rArgs , const css::uno::Reference< css::frame::XFrame >& xNewFrame)
{ // external well known frame must be preferred (because it was created by ourself // for loading documents into this frame)! // But if no frame exists... we can try to locate it using any frame bound to the provided // document. Of course we must live without any frame in case the document does not exists at this // point. But this state should not occur. In such case xNewFrame should be valid ... hopefully .-)
css::uno::Reference< css::frame::XFrame > xFrame = xNewFrame; if (
(!xFrame.is() ) &&
(rInfo.Document.is())
)
{
css::uno::Reference< css::frame::XController > xController = rInfo.Document->getCurrentController(); if (xController.is())
xFrame = xController->getFrame();
}
// stop progress interception on corresponding frame.
css::uno::Reference< css::beans::XPropertySet > xFrameProps(xFrame, css::uno::UNO_QUERY); if (xFrameProps.is())
xFrameProps->setPropertyValue(FRAME_PROPNAME_ASCII_INDICATORINTERCEPTION, css::uno::Any(css::uno::Reference< css::task::XStatusIndicator >()));
// forget progress inside list of arguments.
utl::MediaDescriptor::iterator pArg = rArgs.find(utl::MediaDescriptor::PROP_STATUSINDICATOR); if (pArg != rArgs.end())
{
rArgs.erase(pArg);
pArg = rArgs.end();
}
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.160Bemerkung:
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-09-30)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.