SnapshotImpl.cpp revision 6e4634c400d824266e0a5c756d0a3e3e2eff19a6
/* $Id$ */
/** @file
*
* COM class implementation for Snapshot and SnapshotMachine in VBoxSVC.
*/
/*
* Copyright (C) 2006-2010 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
#include "Logging.h"
#include "SnapshotImpl.h"
#include "MachineImpl.h"
#include "MediumImpl.h"
#include "MediumFormatImpl.h"
#include "Global.h"
#include "ProgressImpl.h"
// @todo these three includes are required for about one or two lines, try
// to remove them and put that code in shared code in MachineImplcpp
#include "SharedFolderImpl.h"
#include "USBControllerImpl.h"
#include "VirtualBoxImpl.h"
#include "AutoCaller.h"
#include <VBox/settings.h>
////////////////////////////////////////////////////////////////////////////////
//
// Globals
//
////////////////////////////////////////////////////////////////////////////////
/**
* Progress callback handler for lengthy operations
* (corresponds to the FNRTPROGRESS typedef).
*
* @param uPercentage Completetion precentage (0-100).
* @param pvUser Pointer to the Progress instance.
*/
{
/* update the progress object */
if (progress)
return VINF_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
//
// Snapshot private data definition
//
////////////////////////////////////////////////////////////////////////////////
{
Data()
: pVirtualBox(NULL)
{
RTTimeSpecSetMilli(&timeStamp, 0);
};
~Data()
{}
/** weak VirtualBox parent */
VirtualBox * const pVirtualBox;
// pParent and llChildren are protected by the machine lock
};
////////////////////////////////////////////////////////////////////////////////
//
// Constructor / destructor
//
////////////////////////////////////////////////////////////////////////////////
{
LogFlowThisFunc(("\n"));
return S_OK;
}
void Snapshot::FinalRelease()
{
LogFlowThisFunc(("\n"));
uninit();
}
/**
* Initializes the instance
*
* @param aId id of the snapshot
* @param aName name of the snapshot
* @param aDescription name of the snapshot (NULL if no description)
* @param aTimeStamp timestamp of the snapshot, in ms since 1970-01-01 UTC
* @param aMachine machine associated with this snapshot
* @param aParent parent snapshot (NULL if no parent)
*/
const Utf8Str &aDescription,
const RTTIMESPEC &aTimeStamp,
{
LogFlowThisFunc(("uuid=%s aParent->uuid=%s\n", aId.toString().c_str(), (aParent) ? aParent->m->uuid.toString().c_str() : ""));
/* Enclose the state transition NotReady->InInit->Ready */
AutoInitSpan autoInitSpan(this);
m = new Data;
/* share parent weakly */
m->strDescription = aDescription;
m->timeStamp = aTimeStamp;
if (aParent)
/* Confirm a successful initialization when it's the case */
return S_OK;
}
/**
* Uninitializes the instance and sets the ready flag to FALSE.
* Called either from FinalRelease(), by the parent when it gets destroyed,
* or by a third party when it decides this object is no more valid.
*
* Since this manipulates the snapshots tree, the caller must hold the
* machine lock in write mode (which protects the snapshots tree)!
*/
{
LogFlowThisFunc(("\n"));
/* Enclose the state transition Ready->InUninit->NotReady */
AutoUninitSpan autoUninitSpan(this);
if (autoUninitSpan.uninitDone())
return;
// uninit all children
++it)
{
}
if (m->pParent)
deparent();
if (m->pMachine)
{
}
delete m;
m = NULL;
}
/**
* Delete the current snapshot by removing it from the tree of snapshots
* and reparenting its children.
*
* After this, the caller must call uninit() on the snapshot. We can't call
* that from here because if we do, the AutoUninitSpan waits forever for
* the number of callers to become 0 (it is 1 because of the AutoCaller in here).
*
* NOTE: this does NOT lock the snapshot, it is assumed that the machine state
* (and the snapshots tree) is protected by the caller having requested the machine
* lock in write mode AND the machine state must be DeletingSnapshot.
*/
void Snapshot::beginSnapshotDelete()
{
AutoCaller autoCaller(this);
return;
// caller must have acquired the machine's write lock
// the snapshot must have only one child when being deleted or no children at all
/// @todo (dmik):
// when we introduce clones later, deleting the snapshot will affect
// the current and first snapshots of clones, if they are direct children
// of this snapshot. So we will need to lock machines associated with
// mFirstSnapshot fields.
{
/* we've changed the base of the current state so mark it as
* modified as it no longer guaranteed to be its copy */
}
{
{
}
else
}
// reparent our children
++it)
{
// no need to lock, snapshots tree is protected by machine lock
if (m->pParent)
}
// clear our own children list (since we reparented the children)
m->llChildren.clear();
}
/**
* Internal helper that removes "this" from the list of children of its
* parent. Used in uninit() and other places when reparenting is necessary.
*
* The caller must hold the machine lock in write mode (which protects the snapshots tree)!
*/
{
++it)
{
if (this == pParentsChild)
{
break;
}
}
}
////////////////////////////////////////////////////////////////////////////////
//
// ISnapshot public methods
//
////////////////////////////////////////////////////////////////////////////////
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
/**
* @note Locks this object for writing, then calls Machine::onSnapshotChange()
* (see its lock requirements).
*/
{
AutoCaller autoCaller(this);
{
// flag the machine as dirty or change won't get saved
return m->pMachine->onSnapshotChange(this);
}
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
if (m->strDescription != strDescription)
{
m->strDescription = strDescription;
// flag the machine as dirty or change won't get saved
return m->pMachine->onSnapshotChange(this);
}
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
return S_OK;
}
{
AutoCaller autoCaller(this);
// snapshots tree is protected by machine lock
return S_OK;
}
////////////////////////////////////////////////////////////////////////////////
//
// Snapshot public internal methods
//
////////////////////////////////////////////////////////////////////////////////
/**
* Returns the parent snapshot or NULL if there's none. Must have caller + locking!
* @return
*/
{
return m->pParent;
}
/**
* Returns the first child snapshot or NULL if there's none. Must have caller + locking!
* @return
*/
{
if (!m->llChildren.size())
return NULL;
return m->llChildren.front();
}
/**
* @note
* Must be called from under the object's lock!
*/
{
}
/**
* @note
* Must be called from under the object's write lock!
*/
{
if (RT_SUCCESS(vrc))
}
/**
* Returns the number of direct child snapshots, without grandchildren.
* Does not recurse.
* @return
*/
{
AutoCaller autoCaller(this);
// snapshots tree is protected by machine lock
}
/**
* Implementation method for getAllChildrenCount() so we request the
* tree lock only once before recursing. Don't call directly.
* @return
*/
{
AutoCaller autoCaller(this);
++it)
{
}
return count;
}
/**
* Returns the number of child snapshots including all grandchildren.
* Recurses into the snapshots tree.
* @return
*/
{
AutoCaller autoCaller(this);
// snapshots tree is protected by machine lock
return getAllChildrenCountImpl();
}
/**
* Returns the SnapshotMachine that this snapshot belongs to.
* Caller must hold the snapshot's object lock!
* @return
*/
{
return m->pMachine;
}
/**
* Returns the UUID of this snapshot.
* Caller must hold the snapshot's object lock!
* @return
*/
{
return m->uuid;
}
/**
* Returns the name of this snapshot.
* Caller must hold the snapshot's object lock!
* @return
*/
{
return m->strName;
}
/**
* Returns the time stamp of this snapshot.
* Caller must hold the snapshot's object lock!
* @return
*/
{
return m->timeStamp;
}
/**
* Searches for a snapshot with the given ID among children, grand-children,
* etc. of this snapshot. This snapshot itself is also included in the search.
*
* Caller must hold the machine lock (which protects the snapshots tree!)
*/
{
AutoCaller autoCaller(this);
// no need to lock, uuid is const
child = this;
else
{
++it)
{
break;
}
}
return child;
}
/**
* Searches for a first snapshot with the given name among children,
* grand-children, etc. of this snapshot. This snapshot itself is also included
* in the search.
*
* Caller must hold the machine lock (which protects the snapshots tree!)
*/
{
AutoCaller autoCaller(this);
child = this;
else
{
++it)
{
break;
}
}
return child;
}
/**
* Internal implementation for Snapshot::updateSavedStatePaths (below).
* @param aOldPath
* @param aNewPath
*/
{
/* state file may be NULL (for offline snapshots) */
)
{
}
++it)
{
}
}
/**
* Checks if the specified path change affects the saved state file path of
* this snapshot or any of its (grand-)children and updates it accordingly.
*
* Intended to be called by Machine::openConfigLoader() only.
*
* @param aOldPath old path (full)
* @param aNewPath new path (full)
*
* @note Locks the machine (for the snapshots tree) + this object + children for writing.
*/
{
AutoCaller autoCaller(this);
// snapshots tree is protected by machine lock
// call the implementation under the tree lock
}
/**
* Internal implementation for Snapshot::saveSnapshot (below). Caller has
* requested the snapshots tree (machine) lock.
*
* @param aNode
* @param aAttrsOnly
* @return
*/
{
if (aAttrsOnly)
return S_OK;
/* stateFile (optional) */
if (!stateFilePath().isEmpty())
else
if (m->llChildren.size())
{
++it)
{
}
}
return S_OK;
}
/**
* Saves the given snapshot and all its children (unless \a aAttrsOnly is true).
* It is assumed that the given node is empty (unless \a aAttrsOnly is true).
*
* @param aNode <Snapshot> node to save the snapshot to.
* @param aSnapshot Snapshot to save.
* @param aAttrsOnly If true, only updatge user-changeable attrs.
*/
{
// snapshots tree is protected by machine lock
}
/**
* Part of the cleanup engine of Machine::Unregister().
*
* This recursively removes all medium attachments from the snapshot's machine
* and returns the snapshot's saved state file name, if any, and then calls
* uninit() on "this" itself.
*
* This recurses into children first, so the given MediaList receives child
* media first before their parents. If the caller wants to close all media,
* they should go thru the list from the beginning to the end because media
* cannot be closed if they have children.
*
* This calls uninit() on itself, so the snapshots tree becomes invalid after this.
* It does not alter the main machine's snapshot pointers (pFirstSnapshot, pCurrentSnapshot).
*
* Caller must hold the machine write lock (which protects the snapshots tree!)
*
* @param llFilenames
* @return
*/
{
// recurse into children first so that the child media appear on
// the list first; this way caller can close the media from the
// beginning to the end because parent media can't be closed if
// they have children
// make a copy of the children list since uninit() modifies it
++it)
{
return rc;
}
// now call detachAllMedia on the snapshot machine
this /* pSnapshot */,
llMedia);
return rc;
// now report the saved state file
this->beginSnapshotDelete();
this->uninit();
return S_OK;
}
////////////////////////////////////////////////////////////////////////////////
//
// SnapshotMachine implementation
//
////////////////////////////////////////////////////////////////////////////////
{
LogFlowThisFunc(("\n"));
return S_OK;
}
void SnapshotMachine::FinalRelease()
{
LogFlowThisFunc(("\n"));
uninit();
}
/**
* Initializes the SnapshotMachine object when taking a snapshot.
*
* @param aSessionMachine machine to take a snapshot from
* @param aSnapshotId snapshot ID of this snapshot machine
* @param aStateFilePath file where the execution state will be later saved
* (or NULL for the offline snapshot)
*
* @note The aSessionMachine must be locked for writing.
*/
const Utf8Str &aStateFilePath)
{
/* Enclose the state transition NotReady->InInit->Ready */
AutoInitSpan autoInitSpan(this);
/* memorize the primary Machine instance (i.e. not SessionMachine!) */
/* share the parent pointer */
/* take the pointer to Data to share */
/* take the pointer to UserData to share (our UserData must always be the
* same as Machine's data) */
/* make a private copy of all other data (recent changes from SessionMachine) */
/* SSData is always unique for SnapshotMachine */
/* create copies of all shared folders (mHWData after attiching a copy
* contains just references to original objects) */
++it)
{
}
/* associate hard disks with the snapshot
* (Machine::uninitDataAndChildObjects() will deassociate at destruction) */
++it)
{
if (pMedium) // can be NULL for non-harddisk
{
}
}
/* create copies of all storage controllers (mStorageControllerData
* after attaching a copy contains just references to original objects) */
++it)
{
ctrl.createObject();
}
/* create all other child objects that will be immutable private copies */
#ifdef VBOX_WITH_VRDP
#endif
{
}
{
}
{
}
/* Confirm a successful initialization when it's the case */
return S_OK;
}
/**
* Initializes the SnapshotMachine object when loading from the settings file.
*
* @param aMachine machine the snapshot belngs to
* @param aHWNode <Hardware> node
* @param aHDAsNode <HardDiskAttachments> node
* @param aSnapshotId snapshot ID of this snapshot machine
* @param aStateFilePath file where the execution state is saved
* (or NULL for the offline snapshot)
*
* @note Doesn't lock anything.
*/
const Utf8Str &aStateFilePath)
{
/* Enclose the state transition NotReady->InInit->Ready */
AutoInitSpan autoInitSpan(this);
/* Don't need to lock aMachine when VirtualBox is starting up */
/* memorize the primary Machine instance */
/* share the parent pointer */
/* take the pointer to Data to share */
/*
* take the pointer to UserData to share
* (our UserData must always be the same as Machine's data)
*/
/* allocate private copies of all other data (will be loaded from settings) */
/* SSData is always unique for SnapshotMachine */
/* create all other child objects that will be immutable private copies */
mBIOSSettings->init(this);
#ifdef VBOX_WITH_VRDP
mVRDPServer->init(this);
#endif
mAudioAdapter->init(this);
mUSBController->init(this);
{
}
{
}
{
}
/* load hardware and harddisk settings */
/* commit all changes made during the initialization */
commit(); // @todo r=dj why do we need a commit in init?!? this is very expensive
/* Confirm a successful initialization when it's the case */
return rc;
}
/**
* Uninitializes this SnapshotMachine object.
*/
void SnapshotMachine::uninit()
{
/* Enclose the state transition Ready->InUninit->NotReady */
AutoUninitSpan autoUninitSpan(this);
if (autoUninitSpan.uninitDone())
return;
/* free the essential data structure last */
}
/**
* Overrides VirtualBoxBase::lockHandle() in order to share the lock handle
* with the primary Machine instance (mPeer).
*/
{
return mPeer->lockHandle();
}
////////////////////////////////////////////////////////////////////////////////
//
// SnapshotMachine public internal methods
//
////////////////////////////////////////////////////////////////////////////////
/**
* Called by the snapshot object associated with this SnapshotMachine when
* snapshot data such as name or description is changed.
*
* @note Locks this object for writing.
*/
{
// mPeer->saveAllSnapshots(); @todo
/* inform callbacks */
return S_OK;
}
////////////////////////////////////////////////////////////////////////////////
//
// SessionMachine task records
//
////////////////////////////////////////////////////////////////////////////////
/**
* Abstract base class for SessionMachine::RestoreSnapshotTask and
* SessionMachine::DeleteSnapshotTask. This is necessary since
* RTThreadCreate cannot call a method as its thread function, so
* instead we have it call the static SessionMachine::taskHandler,
* which can then call the handler() method in here (implemented
* by the children).
*/
struct SessionMachine::SnapshotTask
{
Progress *p,
Snapshot *s)
: pMachine(m),
pProgress(p),
pSnapshot(s)
{}
void modifyBackedUpState(MachineState_T s)
{
*const_cast<MachineState_T*>(&machineStateBackup) = s;
}
virtual void handler() = 0;
const MachineState_T machineStateBackup;
};
/** Restore snapshot state task */
struct SessionMachine::RestoreSnapshotTask
: public SessionMachine::SnapshotTask
{
Progress *p,
Snapshot *s,
: SnapshotTask(m, p, s),
{}
void handler()
{
pMachine->restoreSnapshotHandler(*this);
}
};
/** Delete snapshot task */
struct SessionMachine::DeleteSnapshotTask
: public SessionMachine::SnapshotTask
{
Progress *p,
bool fDeleteOnline,
Snapshot *s)
: SnapshotTask(m, p, s),
{}
void handler()
{
pMachine->deleteSnapshotHandler(*this);
}
bool m_fDeleteOnline;
};
/**
* Static SessionMachine method that can get passed to RTThreadCreate to
* have a thread started for a SnapshotTask. See SnapshotTask above.
*
* This calls either RestoreSnapshotTask::handler() or DeleteSnapshotTask::handler().
*/
{
// it's our responsibility to delete the task
delete task;
return 0;
}
////////////////////////////////////////////////////////////////////////////////
//
// TakeSnapshot methods (SessionMachine and related tasks)
//
////////////////////////////////////////////////////////////////////////////////
/**
* Implementation for IInternalMachineControl::beginTakingSnapshot().
*
* Gets called indirectly from Console::TakeSnapshot, which creates a
* progress object in the client and then starts a thread
* (Console::fntTakeSnapshotWorker) which then calls this.
*
* In other words, the asynchronous work for taking snapshots takes place
* on the _client_ (in the Console). This is different from restoring
* or deleting snapshots, which start threads on the server.
*
* This does the server-side work of taking a snapshot: it creates diffencing
* images for all hard disks attached to the machine and then creates a
* Snapshot object with a corresponding SnapshotMachine to save the VM settings.
*
* The client's fntTakeSnapshotWorker() blocks while this takes place.
* After this returns successfully, fntTakeSnapshotWorker() will begin
* saving the machine state to the snapshot object and reconfigure the
* hard disks.
*
* When the console is done, it calls SessionMachine::EndTakingSnapshot().
*
* @note Locks mParent + this object for writing.
*
* @param aInitiator in: The console on which Console::TakeSnapshot was called.
* @param aName in: The name for the new snapshot.
* @param aDescription in: A description for the new snapshot.
* @param aConsoleProgress in: The console's (client's) progress object.
* @param fTakingSnapshotOnline in: True if an online snapshot is being taken (i.e. machine is running).
* @param aStateFilePath out: name of file in snapshots folder to which the console should write the VM state.
* @return
*/
{
AutoCaller autoCaller(this);
// if this becomes true, we need to call VirtualBox::saveSettings() in the end
bool fNeedsSaveSettings = false;
if ( !fTakingSnapshotOnline
)
{
/* save all current settings to ensure current changes are committed and
* hard disks are fixed up */
// no need to check for whether VirtualBox.xml needs changing since
// we can't have a machine XML rename pending at this point
}
/* create an ID for the snapshot */
snapshotId.create();
/* stateFilePath is null when the machine is not online nor saved */
{
snapshotId.ptr());
/* ensure the directory for the saved state file exists */
}
/* create a snapshot machine object */
/* create a snapshot object */
/* fill in the snapshot data */
try
{
LogFlowThisFunc(("Creating differencing hard disks (online=%d)...\n",
// backup the media data so we can recover if things goes wrong along the day;
// the matching commit() is in fixupMedia() during endSnapshot()
mMediaData.backup();
/* Console::fntTakeSnapshotWorker and friends expects this. */
else
setMachineState(MachineState_Saving); /** @todo Confusing! Saving is used for both online and offline snapshots. */
/* create new differencing hard disks and attach them to this machine */
1, // operation weight; must be the same as in Console::TakeSnapshot()
throw rc;
{
LogFlowThisFunc(("Copying the execution state from '%s' to '%s'...\n",
1); // weight
/* Leave the lock before a lengthy operation (machine is protected
* by "Saving" machine state now) */
/* copy the state file */
0,
if (RT_FAILURE(vrc))
/** @todo r=bird: Delete stateTo when appropriate. */
tr("Could not copy the state file '%s' to '%s' (%Rrc)"),
vrc);
}
}
{
)
// @todo r=dj what with the implicit diff that we created above? this is never cleaned up
}
else
*aStateFilePath = NULL;
// @todo r=dj normally we would need to save the settings if fNeedsSaveSettings was set to true,
// but since we have no error handling that cleans up the diff image that might have gotten created,
// there's no point in saving the disk registry at this point either... this needs fixing.
LogFlowThisFunc(("LEAVE - %Rhrc [%s]\n", rc, Global::stringifyMachineState(mData->mMachineState) ));
return rc;
}
/**
* Implementation for IInternalMachineControl::endTakingSnapshot().
*
* Called by the Console when it's done saving the VM state into the snapshot
* (if online) and reconfiguring the hard disks. See BeginTakingSnapshot() above.
*
* This also gets called if the console part of snapshotting failed after the
* BeginTakingSnapshot() call, to clean up the server side.
*
* @note Locks VirtualBox and this object for writing.
*
* @param aSuccess Whether Console was successful with the client-side snapshot things.
* @return
*/
{
LogFlowThisFunc(("\n"));
AutoCaller autoCaller(this);
)
, E_FAIL);
/*
* Restore the state we had when BeginTakingSnapshot() was called,
* Console::fntTakeSnapshotWorker restores its local copy when we return.
* If the state was Running, then let Console::fntTakeSnapshotWorker do it
* all to avoid races.
*/
)
if (aSuccess)
{
// new snapshot becomes the current one
/* memorize the first snapshot if necessary */
if (!mData->mFirstSnapshot)
// snapshots change, so we know we need to save
if (!fOnline)
/* the machine was powered off or saved when taking a snapshot, so
* reset the mCurrentStateModified flag */
// no need to change for whether VirtualBox.xml needs saving since
// we'll save the global settings below anyway
}
{
/* inform callbacks */
}
else
{
/* delete all differencing hard disks created (this will also attach
* their parents back by rolling back mMediaData) */
/* delete the saved state file (it might have been already created) */
}
/* clear out the snapshot data */
// save VirtualBox.xml (media registry most probably changed with diff image);
// for that we should hold only the VirtualBox lock
mParent->saveSettings();
return rc;
}
////////////////////////////////////////////////////////////////////////////////
//
// RestoreSnapshot methods (SessionMachine and related tasks)
//
////////////////////////////////////////////////////////////////////////////////
/**
* Implementation for IInternalMachineControl::restoreSnapshot().
*
* Gets called from Console::RestoreSnapshot(), and that's basically the
* only thing Console does. Restoring a snapshot happens entirely on the
* server side since the machine cannot be running.
*
* This creates a new thread that does the work and returns a progress
* object to the client which is then returned to the caller of
* Console::RestoreSnapshot().
*
* Actual work then takes place in RestoreSnapshotTask::handler().
*
* @note Locks this + children objects for writing!
*
* @param aInitiator in: rhe console on which Console::RestoreSnapshot was called.
* @param aSnapshot in: the snapshot to restore.
* @param aMachineState in: client-side machine state.
* @param aProgress out: progress object to monitor restore thread.
* @return
*/
{
AutoCaller autoCaller(this);
// machine must not be running
E_FAIL);
// create a progress object. The number of operations is:
// 1 (preparing) + # of hard disks + 1 (if we need to copy the saved state file) */
LogFlowThisFunc(("Going thru snapshot machine attachments to determine progress setup\n"));
++it)
{
{
++ulOpCount;
++ulTotalWeight; // assume one MB weight for each differencing hard disk to manage
LogFlowThisFunc(("op %d: considering hard disk attachment %s\n", ulOpCount, pAttach->getMedium()->getName().c_str()));
}
}
ULONG ulStateFileSizeMB = 0;
{
++ulOpCount; // one for the saved state
if (!RT_SUCCESS(irc))
// if we can't access the file here, then we'll be doomed later also, so fail right away
setError(E_FAIL, tr("Cannot access state file '%s', runtime error, %Rra"), pSnapshot->stateFilePath().c_str(), irc);
if (ullSize == 0) // avoid division by zero
LogFlowThisFunc(("op %d: saved state file '%s' has %RI64 bytes (%d MB)\n",
}
FALSE /* aCancelable */,
1);
/* create and start the task on a separate thread (note that it will not
* start working until we release alock) */
(void*)task,
0,
0,
"RestoreSnap");
if (RT_FAILURE(vrc))
{
delete task;
}
/* set the proper machine state (note: after creating a Task instance) */
/* return the progress to the caller */
/* return the new state to the caller */
return S_OK;
}
/**
* Worker method for the restore snapshot thread created by SessionMachine::RestoreSnapshot().
* This method gets called indirectly through SessionMachine::taskHandler() which then
* calls RestoreSnapshotTask::handler().
*
* The RestoreSnapshotTask contains the progress object returned to the console by
* SessionMachine::RestoreSnapshot, through which progress and results are reported.
*
* @note Locks mParent + this object for writing.
*
* @param aTask Task data.
*/
{
AutoCaller autoCaller(this);
if (!autoCaller.isOk())
{
/* we might have been uninitialized because the session was accidentally
* closed by the client, so don't assert */
tr("The session has been accidentally closed"));
return;
}
bool stateRestored = false;
bool fNeedsGlobalSaveSettings = false;
try
{
/* Discard all current changes to mUserData (name, OSType etc.).
* Note that the machine is powered off, so there is no need to inform
* the direct session. */
if (mData->flModifications)
rollback(false /* aNotify */);
/* Delete the saved state file if the machine was Saved prior to this
* operation */
{
throw rc;
}
{
/* remember the timestamp of the snapshot we're restoring from */
/* copy all hardware data from the snapshot */
LogFlowThisFunc(("Restoring hard disks from the snapshot...\n"));
// restore the attachments from the snapshot
mMediaData.backup();
/* leave the locks before the potentially lengthy operation */
1,
false /* aOnline */,
throw rc;
/* Note: on success, current (old) hard disks will be
* deassociated/deleted on #commit() called from #saveSettings() at
* the end. On failure, newly created implicit diffs will be
* deleted by #rollback() at the end. */
/* should not have a saved state file associated at this point */
{
LogFlowThisFunc(("Copying saved state file from '%s' to '%s'...\n",
/* leave the lock before the potentially lengthy operation */
/* copy the state file */
0,
if (RT_SUCCESS(vrc))
else
tr("Could not copy the state file '%s' to '%s' (%Rrc)"),
stateFilePath.raw(),
vrc);
}
/* make the snapshot we restored from the current snapshot */
}
/* grab differencing hard disks from the old attachments that will
* become unused and need to be auto-deleted */
for (MediaData::AttachmentList::const_iterator it = mMediaData.backedUpData()->mAttachments.begin();
++it)
{
/* while the hard disk is attached, the number of children or the
* parent cannot change, so no lock */
)
{
}
}
int saveFlags = 0;
/* we have already deleted the current state, so set the execution
* state accordingly no matter of the delete snapshot result */
else
stateRestored = true;
/* assign the timestamp from the snapshot */
// detach the current-state diffs that we detected above and build a list of
// image files to delete _after_ saveSettings()
++it)
{
ComObjPtr<MediumAttachment> pAttach = *it; // guaranteed to have only attachments where medium != NULL
LogFlowThisFunc(("Detaching old current state in differencing image '%s'\n", pMedium->getName().raw()));
// Normally we "detach" the medium by removing the attachment object
// from the current machine data; saveSettings() below would then
// compare the current machine data with the one in the backup
// and actually call Medium::detachFrom(). But that works only half
// the time in our case so instead we force a detachment here:
// remove from machine data
// remove it from the backup or else saveSettings will try to detach
// it again and assert
// then clean up backrefs
}
// save machine settings, reset the modified flag and commit;
throw rc;
// let go of the locks while we're deleting image files below
// from here on we cannot roll back on failure any more
++it)
{
LogFlowThisFunc(("Deleting old current state in differencing image '%s'\n", pMedium->getName().raw()));
true /* aWait */,
// ignore errors here because we cannot roll back after saveSettings() above
}
}
{
}
{
/* preserve existing error info */
/* undo all changes on failure */
rollback(false /* aNotify */);
if (!stateRestored)
{
/* restore the machine state */
}
}
{
// finally, VirtualBox.xml needs saving too
mParent->saveSettings();
}
/* set the result (this will try to fetch current error info on failure) */
}
////////////////////////////////////////////////////////////////////////////////
//
// DeleteSnapshot methods (SessionMachine and related tasks)
//
////////////////////////////////////////////////////////////////////////////////
/**
* Implementation for IInternalMachineControl::deleteSnapshot().
*
* Gets called from Console::DeleteSnapshot(), and that's basically the
* only thing Console does initially. Deleting a snapshot happens entirely on
* the server side if the machine is not running, and if it is running then
* the individual merges are done via internal session callbacks.
*
* This creates a new thread that does the work and returns a progress
* object to the client which is then returned to the caller of
* Console::DeleteSnapshot().
*
* Actual work then takes place in DeleteSnapshotTask::handler().
*
* @note Locks mParent + this + children objects for writing!
*/
{
AutoCaller autoCaller(this);
// be very picky about machine states
return setError(VBOX_E_INVALID_VM_STATE,
tr("Invalid machine state: %s"),
if (childrenCount > 1)
return setError(VBOX_E_INVALID_OBJECT_STATE,
tr("Snapshot '%s' of the machine '%ls' cannot be deleted. because it has %d child snapshots, which is more than the one snapshot allowed for deletion"),
/* If the snapshot being deleted is the current one, ensure current
* settings are committed and saved.
*/
{
if (mData->flModifications)
{
// no need to change for whether VirtualBox.xml needs saving since
// we can't have a machine XML rename pending at this point
}
}
/* create a progress object. The number of operations is:
* 1 (preparing) + 1 if the snapshot is online + # of normal hard disks
*/
LogFlowThisFunc(("Going thru snapshot machine attachments to determine progress setup\n"));
{
++ulOpCount;
++ulTotalWeight; // assume 1 MB for deleting the state file
}
// count normal hard disks and add their sizes to the weight
++it)
{
{
// writethrough and shareable images are unaffected by snapshots,
// so do nothing for them
if ( type != MediumType_Writethrough
&& type != MediumType_Shareable)
{
// normal or immutable media need attention
++ulOpCount;
}
LogFlowThisFunc(("op %d: considering hard disk attachment %s\n", ulOpCount, pHD->getName().c_str()));
}
}
FALSE /* aCancelable */,
1);
/* create and start the task on a separate thread */
(void*)task,
0,
0,
"DeleteSnapshot");
if (RT_FAILURE(vrc))
{
delete task;
return E_FAIL;
}
// the task might start running but will block on acquiring the machine's write lock
// which we acquired above; once this function leaves, the task will be unblocked;
// set the proper machine state here now (note: after creating a Task instance)
else
/* return the progress to the caller */
/* return the new state to the caller */
return S_OK;
}
/**
* Helper struct for SessionMachine::deleteSnapshotHandler().
*/
struct MediumDeleteRec
{
: mfNeedsOnlineMerge(false),
{}
bool fMergeForward,
const MediaList &aChildrenToReparent,
bool fNeedsOnlineMerge,
{}
bool fMergeForward,
const MediaList &aChildrenToReparent,
bool fNeedsOnlineMerge,
const Guid &aMachineId,
const Guid &aSnapshotId)
{}
bool mfMergeForward;
bool mfNeedsOnlineMerge;
/* these are for reattaching the hard disk in case of a failure: */
};
/**
* Worker method for the delete snapshot thread created by
* SessionMachine::DeleteSnapshot(). This method gets called indirectly
* through SessionMachine::taskHandler() which then calls
* DeleteSnapshotTask::handler().
*
* The DeleteSnapshotTask contains the progress object returned to the console
* by SessionMachine::DeleteSnapshot, through which progress and results are
* reported.
*
* SessionMachine::DeleteSnapshot() has set the machine state to
* MachineState_DeletingSnapshot right after creating this task. Since we block
* on the machine write lock at the beginning, once that has been acquired, we
* can assume that the machine state is indeed that.
*
* @note Locks the machine + the snapshot + the media tree for writing!
*
* @param aTask Task data.
*/
{
AutoCaller autoCaller(this);
if (!autoCaller.isOk())
{
/* we might have been uninitialized because the session was accidentally
* closed by the client, so don't assert */
tr("The session has been accidentally closed"));
return;
}
bool fMachineSettingsChanged = false; // Machine
bool fNeedsSaveSettings = false; // VirtualBox.xml
try
{
/* Locking order: */
// once we have this lock, we know that SessionMachine::DeleteSnapshot()
// has exited after setting the machine state to MachineState_DeletingSnapshot
// no need to lock the snapshot machine since it is const by definiton
// save the snapshot ID (for callbacks)
// first pass:
LogFlowThisFunc(("1: Checking hard disk merge prerequisites...\n"));
// Go thru the attachments of the snapshot machine (the media in here
// point to the disk states _before_ the snapshot was taken, i.e. the
// state we're restoring to; for each such medium, we will need to
// merge it with its one and only child (the diff image holding the
// changes written after the snapshot was taken).
++it)
{
continue;
{
// writethrough and shareable images are unaffected by
// snapshots, skip them
if ( type == MediumType_Writethrough
|| type == MediumType_Shareable)
continue;
}
#ifdef DEBUG
pHD->dumpBackRefs();
#endif
// needs to be merged with child or deleted, check prerequisites
bool fMergeForward = false;
bool fNeedsOnlineMerge = false;
if (fOnlineMergePossible)
{
// Look up the corresponding medium attachment in the currently
// running VM. Any failure prevents a live merge. Could be made
// a tad smarter by trying a few candidates, so that e.g. disks
// which are simply moved to a different controller slot do not
// prevent online merging in general.
{
fOnlineMergePossible = false;
}
else
fOnlineMergePossible = false;
}
throw rc;
// no need to hold the lock any longer
// For simplicity, prepareDeleteSnapshotMedium selects the merge
// direction in the following way: we merge pHD onto its child
// (forward merge), not the other way round, because that saves us
// from unnecessarily shuffling around the attachments for the
// machine that follows the snapshot (next snapshot or current
// state), unless it's a base image. Backwards merges of the first
// snapshot into the base image is essential, as it ensures that
// when all snapshots are deleted the only remaining image is a
// base image. Important e.g. for medium formats which do not have
// a file representation such as iSCSI.
// a couple paranoia checks for backward merges
{
// parent is null -> this disk is a base hard disk: we will
// then do a backward merge, i.e. merge its only child onto the
// base disk. Here we need then to update the attachment that
// refers to the child and have it point to the parent instead
}
// minimal sanity checking
if (pReplaceMachineId)
if (pSnapshotId)
if (!replaceMachineId.isEmpty())
{
// Adjust the backreferences, otherwise merging will assert.
// Note that the medium attachment object stays associated
// with the snapshot until the merge was successful.
}
else
}
// we can release the lock now since the machine state is MachineState_DeletingSnapshot
/* Now we checked that we can successfully merge all normal hard disks
* (unless a runtime error like end-of-disc happens). Now get rid of
* the saved state (if present), as that will free some disk space.
* The snapshot itself will be deleted as late as possible, so that
* the user can repeat the delete operation if he runs out of disk
* space or cancels the delete operation. */
/* second pass: */
LogFlowThisFunc(("2: Deleting saved state...\n"));
{
// saveAllSnapshots() needs a machine lock, and the snapshots
// tree is protected by the machine lock as well
if (!stateFilePath.isEmpty())
{
1); // weight
fMachineSettingsChanged = true;
}
}
/* third pass: */
LogFlowThisFunc(("3: Performing actual hard disk merging...\n"));
/// @todo NEWMEDIA turn the following errors into warnings because the
/// snapshot itself has been already deleted (and interpret these
/// warnings properly on the GUI side)
{
{
}
ulWeight);
bool fNeedSourceUninit = false;
bool fReparentTarget = false;
{
/* no real merge needed, just updating state and delete
* diff files if necessary */
AutoMultiWriteLock2 mLock(&mParent->getMediaTreeLockHandle(), pMedium->lockHandle() COMMA_LOCKVAL_SRC_POS);
/* Delete the differencing hard disk (has no children). Two
* exceptions: if it's the last medium in the chain or if it's
* a backward merge we don't want to handle due to complextity.
* In both cases leave the image in place. If it's the first
* exception the user can delete it later if he wants. */
{
/* No need to hold the lock any longer. */
bool fNeedsSave = false;
true /* aWait */,
&fNeedsSave);
throw rc;
// need to uninit the deleted medium
fNeedSourceUninit = true;
}
}
else
{
bool fNeedsSave = false;
if (it->mfNeedsOnlineMerge)
{
/// @todo VBoxHDD cannot handle backward merges where source==active disk yet
tr("Snapshot '%s' of the machine '%ls' cannot be deleted while a VM is running, as this case is not implemented yet. You can delete the snapshot when the VM is powered off"),
// online medium merge, in the direction decided earlier
&fNeedsSave);
}
else
{
// normal medium merge, in the direction decided earlier
true /* aWait */,
&fNeedsSave);
}
// If the merge failed, we need to do our best to have a usable
// VM configuration afterwards. The return code doesn't tell
// whether the merge completed and so we have to check if the
// source medium (diff images are always file based at the
// moment) is still there or not. Be careful not to lose the
// error code below, before the "Delayed failure exit".
{
// No medium format description? get out of here.
if (sourceFormat.isNull())
throw rc;
// Diff medium not backed by a file - cannot get status so
// be pessimistic.
throw rc;
// Source medium is still there, so merge failed early.
throw rc;
// Source medium is gone. Assume the merge succeeded and
// thus it's safe to remove the attachment. We use the
// "Delayed failure exit" below.
}
// need to change the medium attachment for backward merges
if (!it->mfNeedsOnlineMerge)
{
// need to uninit the medium deleted by the merge
fNeedSourceUninit = true;
// delete the no longer needed medium lock list, which
// implicitly handled the unlocking
delete it->mpMediumLockList;
}
}
// remove the medium attachment from the snapshot. For a backwards
// merge the target attachment needs to be removed from the
// snapshot, as the VM will take it over. For forward merges the
// source medium attachment needs to be removed.
if (fReparentTarget)
{
}
else
if (fReparentTarget)
{
// Search for old source attachment and replace with target.
// There can be only one child snapshot in this case.
if (pChildSnapshot)
{
}
// If no attachment is found do not change anything. The source
// medium might not have been attached to the snapshot.
if (pAtt)
{
}
}
if (fNeedSourceUninit)
// One attachment is merged, must save the settings
fMachineSettingsChanged = true;
// prevent calling cancelDeleteSnapshotMedium() for this attachment
// Delayed failure exit when the merge cleanup failed but the
// merge actually succeeded.
throw rc;
}
{
// beginSnapshotDelete() needs the machine lock, and the snapshots
// tree is protected by the machine lock as well
fMachineSettingsChanged = true;
}
}
{
// preserve existing error info so that the result can
// be properly reported to the progress object below
// un-prepare the remaining hard disks
++it)
{
it->mSnapshotId);
}
}
// whether we were successful or not, we need to set the machine
// state and save the machine settings;
{
// preserve existing error info so that the result can
// be properly reported to the progress object below
// restore the machine state that was saved when the
// task was started
{
{
/// @todo r=klaus the SaveS_Force is right now a workaround,
// as something in saveSettings fails to detect deleted
// snapshots in some cases (2 child snapshots -> 1 child
// snapshot). Should be fixed, but don't drop SaveS_Force
// then, as it avoids a rather costly config equality check
// when we know that it is changed.
}
if (fNeedsSaveSettings)
{
mParent->saveSettings();
}
}
}
// report the result (this will try to fetch current error info on failure)
}
/**
* performs necessary state changes. Must not be called for writethrough disks
*
* This method is to be called prior to calling #deleteSnapshotMedium().
* If #deleteSnapshotMedium() is not called or fails, the state modifications
* performed by this method must be undone by #cancelDeleteSnapshotMedium().
*
* @return COM status code
* @param aHD Hard disk which is connected to the snapshot.
* @param aMachineId UUID of machine this hard disk is attached to.
* @param aSnapshotId UUID of snapshot this hard disk is attached to. May
* be a zero UUID if no snapshot is applicable.
* @param fOnlineMergePossible Flag whether an online merge is possible.
* @param aVMMALockList Medium lock list for the medium attachment of this VM.
* Only used if @a fOnlineMergePossible is @c true, and
* must be non-NULL in this case.
* @param aSource Source hard disk for merge (out).
* @param aTarget Target hard disk for merge (out).
* @param aMergeForward Merge direction decision (out).
* @param aParentForTarget New parent if target needs to be reparented (out).
* @param aChildrenToReparent Children which have to be reparented to the
* target (out).
* @param fNeedsOnlineMerge Whether this merge needs to be done online (out).
* If this is set to @a true then the @a aVMMALockList
* parameter has been modified and is returned as
* @a aMediumLockList.
* @param aMediumLockList Where to store the created medium lock list (may
* return NULL if no real merge is necessary).
*
* @note Caller must hold media tree lock for writing. This locks this object
* and every medium object on the merge chain for writing.
*/
const Guid &aMachineId,
const Guid &aSnapshotId,
bool fOnlineMergePossible,
bool &aMergeForward,
bool &fNeedsOnlineMerge,
{
// Medium must not be writethrough/shareable at this point
fNeedsOnlineMerge = false;
{
/* This technically is no merge, set those values nevertheless.
* Helps with updating the medium attachments. */
/* special treatment of the last hard disk in the chain: */
{
/* lock only, to prevent any usage until the snapshot deletion
* is completed */
}
/* the differencing hard disk w/o children will be deleted, protect it
* from attaching to other VMs (this is why Deleting) */
return aHD->markForDeletion();
}
/* not going multi-merge as it's too expensive */
tr("Hard disk '%s' has more than one child hard disk (%d)"),
/* we keep this locked, so lock the affected child to make sure the lock
* order is correct when calling prepareMergeTo() */
/* the rest is a normal merge setup */
{
/* base hard disk, backward merge */
{
/* backward merge is too tricky, we'll just detach on snapshot
* deletion, so lock only, to prevent any usage */
}
}
else
{
/* forward merge */
}
!fOnlineMergePossible /* fLockMedia */,
{
/* Try to lock the newly constructed medium lock list. If it succeeds
* this can be handled as an offline merge, i.e. without the need of
* asking the VM to do the merging. Only continue with the online
* merging preparation if applicable. */
{
/* Locking failed, this cannot be done as an offline merge. Try to
* combine the locking information into the lock list of the medium
* attachment in the running VM. If that fails or locking the
* resulting lock list fails then the merge cannot be done online.
* It can be repeated by the user when the VM is shut down. */
aVMMALockList->GetEnd();
it2 = lockListBegin;
it2 != lockListEnd;
{
if ( it == lockListVMMAEnd
{
fOnlineMergePossible = false;
break;
}
{
// could not update the lock, trigger cleanup below
fOnlineMergePossible = false;
break;
}
}
if (fOnlineMergePossible)
{
/* we will lock the children of the source for reparenting */
++it)
{
{
throw rc;
}
else
{
{
throw rc;
}
}
}
}
if (fOnlineMergePossible)
{
{
tr("Cannot lock hard disk '%s' for a live merge"),
}
else
{
delete aMediumLockList;
fNeedsOnlineMerge = true;
}
}
else
{
tr("Failed to construct lock list for a live merge of hard disk '%s'"),
}
// fix the VM's lock list if anything failed
{
lockListLast--;
it != lockListVMMAEnd;
++it)
{
// blindly apply this, only needed for medium objects which
// would be deleted as part of the merge
}
}
}
else
{
tr("Cannot lock hard disk '%s' for an offline merge"),
}
}
return rc;
}
/**
* what #prepareDeleteSnapshotMedium() did. Must be called if
* #deleteSnapshotMedium() is not called or fails.
*
* @param aHD Hard disk which is connected to the snapshot.
* @param aSource Source hard disk for merge.
* @param aChildrenToReparent Children to unlock.
* @param fNeedsOnlineMerge Whether this merge needs to be done online.
* @param aMediumLockList Medium locks to cancel.
* @param aMachineId Machine id to attach the medium to.
* @param aSnapshotId Snapshot id to attach the medium to.
*
* @note Locks the medium tree and the hard disks in the chain for writing.
*/
const MediaList &aChildrenToReparent,
bool fNeedsOnlineMerge,
const Guid &aMachineId,
const Guid &aSnapshotId)
{
if (aMediumLockList == NULL)
{
AutoMultiWriteLock2 mLock(&mParent->getMediaTreeLockHandle(), aHD->lockHandle() COMMA_LOCKVAL_SRC_POS);
{
}
else
{
}
}
else
{
if (fNeedsOnlineMerge)
{
// Online merge uses the medium lock list of the VM, so give
// an empty list to cancelMergeTo so that it works as designed.
// clean up the VM medium lock list ourselves
lockListLast--;
it != lockListEnd;
++it)
{
else
{
// blindly apply this, only needed for medium objects which
// would be deleted as part of the merge
}
}
}
else
{
}
}
if (!aMachineId.isEmpty())
{
// reattach the source media to the snapshot
}
}
/**
* Perform an online merge of a hard disk, i.e. the equivalent of
* Medium::mergeTo(), just for running VMs. If this fails you need to call
* #cancelDeleteSnapshotMedium().
*
* @return COM status code
* @param aMediumAttachment Identify where the disk is attached in the VM.
* @param aSource Source hard disk for merge.
* @param aTarget Target hard disk for merge.
* @param aMergeForward Merge direction.
* @param aParentForTarget New parent if target needs to be reparented.
* @param aChildrenToReparent Children which have to be reparented to the
* target.
* @param aMediumLockList Where to store the created medium lock list (may
* return NULL if no real merge is necessary).
* @param aProgress Progress indicator.
* @param pfNeedsSaveSettings Whether the VM settings need to be saved (out).
*/
bool fMergeForward,
const MediaList &aChildrenToReparent,
bool *pfNeedsSaveSettings)
{
try
{
// Similar code appears in Medium::taskMergeHandle, so
// if you make any changes below check whether they are applicable
// in that context as well.
unsigned uTargetIdx = (unsigned)-1;
unsigned uSourceIdx = (unsigned)-1;
/* Sanity check all hard disks in the chain. */
unsigned i = 0;
it != lockListEnd;
++it)
{
uSourceIdx = i;
uTargetIdx = i;
// In Medium::taskMergeHandler there is lots of consistency
// checking which we cannot do here, as the state details are
// impossible to get outside the Medium class. The locking should
// have done the checks already.
i++;
}
// For forward merges, tell the VM what images need to have their
// parent UUID updated. This cannot be done in VBoxSVC, as opening
// the required parent images is not safe while the VM is running.
// For backward merges this will be simply an array of size 0.
{
throw setError(VBOX_E_INVALID_VM_STATE,
tr("Machine is not locked by a session (session state: %s)"),
}
// Must not hold any locks here, as this will call back to finish
// updating the medium attachment, chain linking and state.
throw rc;
}
// The callback mentioned above takes care of update the medium state
if (pfNeedsSaveSettings)
*pfNeedsSaveSettings = true;
return rc;
}
/**
* Implementation for IInternalMachineControl::finishOnlineMergeMedium().
*
* Gets called after the successful completion of an online merge from
* Console::onlineMergeMedium(), which gets invoked indirectly above in
* the call to IInternalSessionControl::onlineMergeMedium.
*
* This updates the medium information and medium state so that the VM
* can continue with the updated state of the medium chain.
*/
{
// all hard disks but the target were successfully deleted by
// the merge; reparent target if necessary and uninitialize media
if (aMergeForward)
{
// first, unregister the target since it may become a base
// hard disk which needs re-registration
// then, reparent it and disconnect the deleted branch at
// both ends (chain->parent() is source's parent)
if (pParentForTarget)
// then, register again
}
else
{
// disconnect the deleted branch at the elder end
targetChild->deparent();
// Update parent UUIDs of the source's children, reparent them and
// disconnect the deleted branch at the younger end
if (childrenToReparent.size() > 0)
{
// Fix the parent UUID of the images which needs to be moved to
// underneath target. The running machine has the images opened,
// but only for reading since the VM is paused. If anything fails
// we must continue. The worst possible result is that the images
// need manual fixing via VBoxManage to adjust the parent UUID.
{
}
// obey {parent,child} lock order
{
}
}
}
/* unregister and uninitialize all hard disks removed by the merge */
it != lockListEnd;
)
{
/* Create a real copy of the medium pointer, as the medium
* lock deletion below would invalidate the referenced object. */
/* The target and all images not merged (readonly) are skipped */
{
++it;
}
else
{
NULL /*pfNeedsSaveSettings*/);
/* now, uninitialize the deleted hard disk (note that
* due to the Deleting state, uninit() will not touch
* the parent-child relationship so we need to
* uninitialize each disk individually) */
/* note that the operation initiator hard disk (which is
* normally also the source hard disk) is a special case
* -- there is one more caller added by Task to it which
* we must release. Also, if we are in sync mode, the
* caller may still hold an AutoCaller instance for it
* and therefore we cannot uninit() it (it's therefore
* the caller's responsibility) */
{
}
/* Delete the medium lock list entry, which also releases the
* caller added by MergeChain before uninit() and updates the
* iterator to point to the right place. */
}
/* Stop as soon as we reached the last medium affected by the merge.
* The remaining images must be kept unchanged. */
break;
}
/* Could be in principle folded into the previous loop, but let's keep
* things simple. Update the medium locking to be the standard state:
* all parent images locked for reading, just the last diff for writing. */
lockListLast--;
it != lockListEnd;
++it)
{
}
return S_OK;
}