/** @file * * VirtualBox COM class implementation */ /* * Copyright (C) 2008 Sun Microsystems, Inc. * * This file is part of VirtualBox Open Source Edition (OSE), as * available from http://www.virtualbox.org. This file is free software; * you can redistribute it and/or modify it under the terms of the GNU * 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. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 USA or visit http://www.sun.com if you need * additional information or have any questions. */ #include "HardDisk2Impl.h" #include "VirtualBoxImpl.h" #include "ProgressImpl.h" #include "SystemPropertiesImpl.h" #include "Logging.h" #include #include #include #include #include #include #include #include //////////////////////////////////////////////////////////////////////////////// // Globals //////////////////////////////////////////////////////////////////////////////// /** * Asynchronous task thread parameter bucket. * * Note that instances of this class must be created using new() because the * task thread function will delete them when the task is complete! * * @note The constructor of this class adds a caller on the managed HardDisk2 * object which is automatically released upon destruction. */ struct HardDisk2::Task : public com::SupportErrorInfoBase { enum Operation { CreateDynamic, CreateFixed, CreateDiff, Merge, Delete }; HardDisk2 *that; VirtualBoxBaseProto::AutoCaller autoCaller; ComObjPtr progress; Operation operation; /** Where to save the result when executed using #runNow(). */ HRESULT rc; Task (HardDisk2 *aThat, Progress *aProgress, Operation aOperation) : that (aThat), autoCaller (aThat) , progress (aProgress) , operation (aOperation) , rc (S_OK) {} ~Task(); void setData (HardDisk2 *aTarget) { d.target = aTarget; HRESULT rc = d.target->addCaller(); AssertComRC (rc); } void setData (MergeChain *aChain) { AssertReturnVoid (aChain != NULL); d.chain.reset (aChain); } HRESULT startThread(); HRESULT runNow(); struct Data { Data() : size (0) {} /* CreateDynamic, CreateStatic */ uint64_t size; /* CreateDiff */ ComObjPtr target; /* Merge */ /** Hard disks to merge, in {parent,child} order */ std::auto_ptr chain; } d; protected: // SupportErrorInfoBase interface const GUID &mainInterfaceID() const { return COM_IIDOF (IHardDisk2); } const char *componentName() const { return HardDisk2::ComponentName(); } }; HardDisk2::Task::~Task() { /* remove callers added by setData() */ if (!d.target.isNull()) d.target->releaseCaller(); } /** * Starts a new thread driven by the HardDisk2::taskThread() function and passes * this Task instance as an argument. * * Note that if this method returns success, this Task object becomes an ownee * of the started thread and will be automatically deleted when the thread * terminates. * * @note When the task is executed by this method, IProgress::notifyComplete() * is automatically called for the progress object associated with this * task when the task is finished to signal the operation completion for * other threads asynchronously waiting for it. */ HRESULT HardDisk2::Task::startThread() { int vrc = RTThreadCreate (NULL, HardDisk2::taskThread, this, 0, RTTHREADTYPE_MAIN_HEAVY_WORKER, 0, "HardDisk::Task"); ComAssertMsgRCRet (vrc, ("Could not create HardDisk::Task thread (%Rrc)\n", vrc), E_FAIL); return S_OK; } /** * Runs HardDisk2::taskThread() by passing it this Task instance as an argument * on the current thread instead of creating a new one. * * This call implies that it is made on another temporary thread created for * some asynchronous task. Avoid calling it from a normal thread since the task * operatinos are potentially lengthy and will block the calling thread in this * case. * * Note that this Task object will be deleted by taskThread() when this method * returns! * * @note When the task is executed by this method, IProgress::notifyComplete() * is not called for the progress object associated with this task when * the task is finished. Instead, the result of the operation is returned * by this method directly and it's the caller's responsibility to * complete the progress object in this case. */ HRESULT HardDisk2::Task::runNow() { HardDisk2::taskThread (NIL_RTTHREAD, this); return rc; } //////////////////////////////////////////////////////////////////////////////// /** * Helper class for merge operations. */ class HardDisk2::MergeChain : public HardDisk2::List, public com::SupportErrorInfoBase { public: MergeChain (bool aForward, bool aIgnoreAttachments) : mForward (aForward) , mIgnoreAttachments (aIgnoreAttachments) {} ~MergeChain() { for (iterator it = mChildren.begin(); it != mChildren.end(); ++ it) { HRESULT rc = (*it)->UnlockWrite (NULL); AssertComRC (rc); (*it)->releaseCaller(); } for (iterator it = begin(); it != end(); ++ it) { AutoWriteLock alock (*it); Assert ((*it)->m.state == MediaState_LockedWrite || (*it)->m.state == MediaState_Deleting); if ((*it)->m.state == MediaState_LockedWrite) (*it)->UnlockWrite (NULL); else (*it)->m.state = MediaState_Created; (*it)->releaseCaller(); } if (!mParent.isNull()) mParent->releaseCaller(); } HRESULT addSource (HardDisk2 *aHardDisk) { HRESULT rc = aHardDisk->addCaller(); CheckComRCReturnRC (rc); AutoWriteLock alock (aHardDisk); if (mForward) { rc = checkChildrenAndAttachmentsAndImmutable (aHardDisk); if (FAILED (rc)) { aHardDisk->releaseCaller(); return rc; } } /* go to Deleting */ switch (aHardDisk->m.state) { case MediaState_Created: aHardDisk->m.state = MediaState_Deleting; break; default: aHardDisk->releaseCaller(); return aHardDisk->setStateError(); } push_front (aHardDisk); if (mForward) { /* we will need parent to reparent target */ if (!aHardDisk->mParent.isNull()) { rc = aHardDisk->mParent->addCaller(); CheckComRCReturnRC (rc); mParent = aHardDisk->mParent; } } else { /* we will need to reparent children */ for (List::const_iterator it = aHardDisk->children().begin(); it != aHardDisk->children().end(); ++ it) { rc = (*it)->addCaller(); CheckComRCReturnRC (rc); rc = (*it)->LockWrite (NULL); if (FAILED (rc)) { (*it)->releaseCaller(); return rc; } mChildren.push_back (*it); } } return S_OK; } HRESULT addTarget (HardDisk2 *aHardDisk) { HRESULT rc = aHardDisk->addCaller(); CheckComRCReturnRC (rc); AutoWriteLock alock (aHardDisk); if (!mForward) { rc = checkChildrenAndImmutable (aHardDisk); if (FAILED (rc)) { aHardDisk->releaseCaller(); return rc; } } /* go to LockedWrite */ rc = aHardDisk->LockWrite (NULL); if (FAILED (rc)) { aHardDisk->releaseCaller(); return rc; } push_front (aHardDisk); return S_OK; } HRESULT addIntermediate (HardDisk2 *aHardDisk) { HRESULT rc = aHardDisk->addCaller(); CheckComRCReturnRC (rc); AutoWriteLock alock (aHardDisk); rc = checkChildrenAndAttachments (aHardDisk); if (FAILED (rc)) { aHardDisk->releaseCaller(); return rc; } /* go to Deleting */ switch (aHardDisk->m.state) { case MediaState_Created: aHardDisk->m.state = MediaState_Deleting; break; default: aHardDisk->releaseCaller(); return aHardDisk->setStateError(); } push_front (aHardDisk); return S_OK; } bool isForward() const { return mForward; } HardDisk2 *parent() const { return mParent; } const List &children() const { return mChildren; } HardDisk2 *source() const { AssertReturn (size() > 0, NULL); return mForward ? front() : back(); } HardDisk2 *target() const { AssertReturn (size() > 0, NULL); return mForward ? back() : front(); } protected: // SupportErrorInfoBase interface const GUID &mainInterfaceID() const { return COM_IIDOF (IHardDisk2); } const char *componentName() const { return HardDisk2::ComponentName(); } private: HRESULT check (HardDisk2 *aHardDisk, bool aChildren, bool aAttachments, bool aImmutable) { if (aChildren) { /* not going to multi-merge as it's too expensive */ if (aHardDisk->children().size() > 1) { return setError (E_FAIL, tr ("Hard disk '%ls' involved in the merge operation " "has more than one child hard disk (%d)"), aHardDisk->m.locationFull.raw(), aHardDisk->children().size()); } } if (aAttachments && !mIgnoreAttachments) { if (aHardDisk->m.backRefs.size() != 0) return setError (E_FAIL, tr ("Hard disk '%ls' is attached to %d virtual machines"), aHardDisk->m.locationFull.raw(), aHardDisk->m.backRefs.size()); } if (aImmutable) { if (aHardDisk->mm.type == HardDiskType_Immutable) return setError (E_FAIL, tr ("Hard disk '%ls' is immutable"), aHardDisk->m.locationFull.raw()); } return S_OK; } HRESULT checkChildren (HardDisk2 *aHardDisk) { return check (aHardDisk, true, false, false); } HRESULT checkChildrenAndImmutable (HardDisk2 *aHardDisk) { return check (aHardDisk, true, false, true); } HRESULT checkChildrenAndAttachments (HardDisk2 *aHardDisk) { return check (aHardDisk, true, true, false); } HRESULT checkChildrenAndAttachmentsAndImmutable (HardDisk2 *aHardDisk) { return check (aHardDisk, true, true, true); } /** true if forward merge, false if backward */ bool mForward : 1; /** true to not perform attachment checks */ bool mIgnoreAttachments : 1; /** Parent of the source when forward merge (if any) */ ComObjPtr mParent; /** Children of the source when backward merge (if any) */ List mChildren; }; //////////////////////////////////////////////////////////////////////////////// // HardDisk2 class //////////////////////////////////////////////////////////////////////////////// // constructor / destructor //////////////////////////////////////////////////////////////////////////////// DEFINE_EMPTY_CTOR_DTOR (HardDisk2) HRESULT HardDisk2::FinalConstruct() { /* Initialize the callbacks of the VD error interface */ mm.vdIfCallsError.cbSize = sizeof (VDINTERFACEERROR); mm.vdIfCallsError.enmInterface = VDINTERFACETYPE_ERROR; mm.vdIfCallsError.pfnError = vdErrorCall; /* Initialize the callbacks of the VD progress interface */ mm.vdIfCallsProgress.cbSize = sizeof (VDINTERFACEPROGRESS); mm.vdIfCallsProgress.enmInterface = VDINTERFACETYPE_PROGRESS; mm.vdIfCallsProgress.pfnProgress = vdProgressCall; /* Initialize the per-disk interface chain */ int vrc; vrc = VDInterfaceAdd (&mm.vdIfError, "HardDisk2::vdInterfaceError", VDINTERFACETYPE_ERROR, &mm.vdIfCallsError, this, &mm.vdDiskIfaces); AssertRCReturn (vrc, E_FAIL); vrc = VDInterfaceAdd (&mm.vdIfProgress, "HardDisk2::vdInterfaceProgress", VDINTERFACETYPE_PROGRESS, &mm.vdIfCallsProgress, this, &mm.vdDiskIfaces); AssertRCReturn (vrc, E_FAIL); return S_OK; } void HardDisk2::FinalRelease() { uninit(); } // public initializer/uninitializer for internal purposes only //////////////////////////////////////////////////////////////////////////////// /** * Initializes the hard disk object without creating or opening an associated * storage unit. * * @param aVirtualBox VirtualBox object. * @param aLocaiton Storage unit location. */ HRESULT HardDisk2::init (VirtualBox *aVirtualBox, const BSTR aFormat, const BSTR aLocation) { AssertReturn (aVirtualBox, E_INVALIDARG); AssertReturn (aLocation, E_INVALIDARG); /* Enclose the state transition NotReady->InInit->Ready */ AutoInitSpan autoInitSpan (this); AssertReturn (autoInitSpan.isOk(), E_UNEXPECTED); HRESULT rc = S_OK; /* share VirtualBox weakly (parent remains NULL so far) */ unconst (mVirtualBox) = aVirtualBox; /* register with VirtualBox early, since uninit() will * unconditionally unregister on failure */ aVirtualBox->addDependentChild (this); /* no storage yet */ m.state = MediaState_NotCreated; rc = setLocation (aLocation); CheckComRCReturnRC (rc); /* No storage unit is created yet, no need to queryInfo() */ /// @todo NEWMEDIA check the format ID is valid unconst (mm.format) = aFormat; /* Confirm a successful initialization when it's the case */ if (SUCCEEDED (rc)) autoInitSpan.setSucceeded(); return rc; } /** * Initializes the hard disk object by opening the storage unit at the specified * location. * * Note that the UUID, format and the parent of this hard disk will be * determined when reading the hard disk storage unit. If the detected parent is * not known to VirtualBox, then this method will fail. * * @param aVirtualBox VirtualBox object. * @param aLocaiton Storage unit location. */ HRESULT HardDisk2::init (VirtualBox *aVirtualBox, const BSTR aLocation) { AssertReturn (aVirtualBox, E_INVALIDARG); AssertReturn (aLocation, E_INVALIDARG); /* Enclose the state transition NotReady->InInit->Ready */ AutoInitSpan autoInitSpan (this); AssertReturn (autoInitSpan.isOk(), E_UNEXPECTED); HRESULT rc = S_OK; /* share VirtualBox weakly (parent remains NULL so far) */ unconst (mVirtualBox) = aVirtualBox; /* register with VirtualBox early, since uninit() will * unconditionally unregister on failure */ aVirtualBox->addDependentChild (this); /* there must be a storage unit */ m.state = MediaState_Created; rc = setLocation (aLocation); CheckComRCReturnRC (rc); /* get all the information about the medium from the storage unit */ rc = queryInfo(); if (SUCCEEDED (rc)) { /* if the storage unit is not accessible, it's not acceptable for the * newly opened media so convert this into an error */ if (m.state == MediaState_Inaccessible) { Assert (!m.lastAccessError.isNull()); rc = setError (E_FAIL, Utf8Str (m.lastAccessError)); } } /* storage format must be detected by queryInfo() if the medium is * accessible */ AssertReturn (m.state == MediaState_Inaccessible || (!m.id.isEmpty() && !mm.format.isNull()), E_FAIL); /* Confirm a successful initialization when it's the case */ if (SUCCEEDED (rc)) autoInitSpan.setSucceeded(); return rc; } /** * Initializes the hard disk object by loading its data from the given settings * node. * * @param aVirtualBox VirtualBox object. * @param aParent Parent hard disk or NULL for a root hard disk. * @param aNode settings node. */ HRESULT HardDisk2::init (VirtualBox *aVirtualBox, HardDisk2 *aParent, const settings::Key &aNode) { using namespace settings; AssertReturn (aVirtualBox, E_INVALIDARG); /* Enclose the state transition NotReady->InInit->Ready */ AutoInitSpan autoInitSpan (this); AssertReturn (autoInitSpan.isOk(), E_UNEXPECTED); HRESULT rc = S_OK; /* share VirtualBox and parent weakly */ unconst (mVirtualBox) = aVirtualBox; mParent = aParent; /* register with VirtualBox/parent early, since uninit() will * unconditionally unregister on failure */ if (!aParent) aVirtualBox->addDependentChild (this); else aParent->addDependentChild (this); /* see below why we don't call queryInfo() (and therefore treat the medium * as inaccessible for now */ m.state = MediaState_Inaccessible; /* required */ unconst (m.id) = aNode.value ("uuid"); /* required */ Bstr location = aNode.stringValue ("location"); rc = setLocation (location); CheckComRCReturnRC (rc); /* optional */ { settings::Key descNode = aNode.findKey ("Description"); if (!descNode.isNull()) m.description = descNode.keyStringValue(); } /* required */ unconst (mm.format) = aNode.stringValue ("format"); AssertReturn (!mm.format.isNull(), E_FAIL); /* only for base hard disks */ if (mParent.isNull()) { const char *type = aNode.stringValue ("type"); if (strcmp (type, "Normal") == 0) mm.type = HardDiskType_Normal; else if (strcmp (type, "Immutable") == 0) mm.type = HardDiskType_Immutable; else if (strcmp (type, "Writethrough") == 0) mm.type = HardDiskType_Writethrough; else AssertFailed(); } LogFlowThisFunc (("m.location='%ls', mm.format=%ls, m.id={%RTuuid}\n", m.location.raw(), mm.format.raw(), m.id.raw())); LogFlowThisFunc (("m.locationFull='%ls'\n", m.locationFull.raw())); /* Don't call queryInfo() for registered media to prevent the calling * thread (i.e. the VirtualBox server startup thread) from an unexpected * freeze but mark it as initially inaccessible instead. The vital UUID, * location and format properties are read from the registry file above; to * get the actual state and the the rest of data, the user will have to call * COMGETTER(State). */ /* load all children */ Key::List hardDisks = aNode.keys ("HardDisk"); for (Key::List::const_iterator it = hardDisks.begin(); it != hardDisks.end(); ++ it) { ComObjPtr hardDisk; hardDisk.createObject(); rc = hardDisk->init (aVirtualBox, this, *it); CheckComRCBreakRC (rc); rc = mVirtualBox->registerHardDisk2 (hardDisk, false /* aSaveRegistry */); CheckComRCBreakRC (rc); } /* Confirm a successful initialization when it's the case */ if (SUCCEEDED (rc)) autoInitSpan.setSucceeded(); return rc; } /** * Uninitializes the instance. * * Called either from FinalRelease() or by the parent when it gets destroyed. * * @note All children of this hard disk get uninitialized by calling their * uninit() methods. */ void HardDisk2::uninit() { /* Enclose the state transition Ready->InUninit->NotReady */ AutoUninitSpan autoUninitSpan (this); if (autoUninitSpan.uninitDone()) return; if (m.state == MediaState_Deleting) { /* we are being uninitialized after've been deleted by merge. * Reparenting has already been done so don't touch it here (we are * now orphans and remoeDependentChild() will assert) */ } else { uninitDependentChildren(); if (mParent) mParent->removeDependentChild (this); else mVirtualBox->removeDependentChild (this); } mParent.setNull(); unconst (mVirtualBox).setNull(); } // IHardDisk2 properties //////////////////////////////////////////////////////////////////////////////// STDMETHODIMP HardDisk2::COMGETTER(Format) (BSTR *aFormat) { if (aFormat == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); /* no need to lock, mm.format is const */ mm.format.cloneTo (aFormat); return S_OK; } STDMETHODIMP HardDisk2::COMGETTER(Type) (HardDiskType_T *aType) { if (aType == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); *aType = mm.type; return S_OK; } STDMETHODIMP HardDisk2::COMSETTER(Type) (HardDiskType_T aType) { AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); /* VirtualBox::saveSettings() needs a write lock */ AutoMultiWriteLock2 alock (mVirtualBox, this); switch (m.state) { case MediaState_Created: case MediaState_Inaccessible: break; default: return setStateError(); } if (mm.type == aType) { /* Nothing to do */ return S_OK; } /* cannot change the type of a differencing hard disk */ if (!mParent.isNull()) return setError (E_FAIL, tr ("Hard disk '%ls' is a differencing hard disk"), m.locationFull.raw()); /* cannot change the type of a hard disk being in use */ if (m.backRefs.size() != 0) return setError (E_FAIL, tr ("Hard disk '%ls' is attached to %d virtual machines"), m.locationFull.raw(), m.backRefs.size()); switch (aType) { case HardDiskType_Normal: case HardDiskType_Immutable: { /* normal can be easily converted to imutable and vice versa even * if they have children as long as they are not attached to any * machine themselves */ break; } case HardDiskType_Writethrough: { /* cannot change to writethrough if there are children */ if (children().size() != 0) return setError (E_FAIL, tr ("Hard disk '%ls' has %d child hard disks"), children().size()); break; } default: AssertFailedReturn (E_FAIL); } mm.type = aType; HRESULT rc = mVirtualBox->saveSettings(); return rc; } STDMETHODIMP HardDisk2::COMGETTER(Parent) (IHardDisk2 **aParent) { if (aParent == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); mParent.queryInterfaceTo (aParent); return S_OK; } STDMETHODIMP HardDisk2::COMGETTER(Children) (ComSafeArrayOut (IHardDisk2 *, aChildren)) { if (ComSafeArrayOutIsNull (aChildren)) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); SafeIfaceArray children (this->children()); children.detachTo (ComSafeArrayOutArg (aChildren)); return S_OK; } STDMETHODIMP HardDisk2::COMGETTER(Root) (IHardDisk2 **aRoot) { if (aRoot == NULL) return E_POINTER; /* root() will do callers/locking */ root().queryInterfaceTo (aRoot); return S_OK; } STDMETHODIMP HardDisk2::COMGETTER(ReadOnly) (BOOL *aReadOnly) { if (aReadOnly == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); /* isRadOnly() will do locking */ *aReadOnly = isReadOnly(); return S_OK; } STDMETHODIMP HardDisk2::COMGETTER(LogicalSize) (ULONG64 *aLogicalSize) { if (aLogicalSize == NULL) return E_POINTER; { AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); if (mParent.isNull()) { *aLogicalSize = mm.logicalSize; return S_OK; } } /* We assume that some backend may decide to return a meaningless value in * response to VDGetSize() for differencing hard disks and therefore * always ask the base hard disk ourselves. */ /* root() will do callers/locking */ return root()->COMGETTER (LogicalSize) (aLogicalSize); } // IHardDisk2 methods //////////////////////////////////////////////////////////////////////////////// STDMETHODIMP HardDisk2::CreateDynamicStorage (ULONG64 aLogicalSize, IProgress **aProgress) { if (aProgress == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoWriteLock alock (this); switch (m.state) { case MediaState_NotCreated: break; default: return setStateError(); } /// @todo NEWMEDIA use backend capabilities to decide if dynamic storage /// is supported ComObjPtr progress; progress.createObject(); HRESULT rc = progress->init (mVirtualBox, static_cast (this), BstrFmt (tr ("Creating dynamic hard disk storage unit '%ls'"), m.location.raw()), FALSE /* aCancelable */); CheckComRCReturnRC (rc); /* setup task object and thread to carry out the operation * asynchronously */ std::auto_ptr task (new Task (this, progress, Task::CreateDynamic)); AssertComRCReturnRC (task->autoCaller.rc()); task->d.size = aLogicalSize; rc = task->startThread(); CheckComRCReturnRC (rc); /* go to Creating state on success */ m.state = MediaState_Creating; /* task is now owned by taskThread() so release it */ task.release(); /* return progress to the caller */ progress.queryInterfaceTo (aProgress); return S_OK; } STDMETHODIMP HardDisk2::CreateFixedStorage (ULONG64 aLogicalSize, IProgress **aProgress) { if (aProgress == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoWriteLock alock (this); switch (m.state) { case MediaState_NotCreated: break; default: return setStateError(); } ComObjPtr progress; progress.createObject(); HRESULT rc = progress->init (mVirtualBox, static_cast (this), BstrFmt (tr ("Creating fixed hard disk storage unit '%ls'"), m.location.raw()), FALSE /* aCancelable */); CheckComRCReturnRC (rc); /* setup task object and thread to carry out the operation * asynchronously */ std::auto_ptr task (new Task (this, progress, Task::CreateFixed)); AssertComRCReturnRC (task->autoCaller.rc()); task->d.size = aLogicalSize; rc = task->startThread(); CheckComRCReturnRC (rc); /* go to Creating state on success */ m.state = MediaState_Creating; /* task is now owned by taskThread() so release it */ task.release(); /* return progress to the caller */ progress.queryInterfaceTo (aProgress); return S_OK; } STDMETHODIMP HardDisk2::DeleteStorage (IProgress **aProgress) { if (aProgress == NULL) return E_POINTER; ComObjPtr progress; HRESULT rc = deleteStorageNoWait (progress); if (SUCCEEDED (rc)) { /* return progress to the caller */ progress.queryInterfaceTo (aProgress); } return rc; } STDMETHODIMP HardDisk2::CreateDiffStorage (IHardDisk2 *aTarget, IProgress **aProgress) { if (aTarget == NULL) return E_INVALIDARG; if (aProgress == NULL) return E_POINTER; AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); ComObjPtr diff; HRESULT rc = mVirtualBox->cast (aTarget, diff); CheckComRCReturnRC (rc); AutoWriteLock alock (this); if (mm.type == HardDiskType_Writethrough) return setError (E_FAIL, tr ("Hard disk '%ls' is Writethrough")); /* We want to be locked for reading as long as our diff child is being * created */ rc = LockRead (NULL); CheckComRCReturnRC (rc); ComObjPtr progress; rc = createDiffStorageNoWait (diff, progress); if (FAILED (rc)) { HRESULT rc2 = UnlockRead (NULL); AssertComRC (rc2); /* Note: on success, taskThread() will unlock this */ } else { /* return progress to the caller */ progress.queryInterfaceTo (aProgress); } return rc; } STDMETHODIMP HardDisk2::MergeTo (INPTR GUIDPARAM aTargetId, IProgress **aProgress) { AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); return E_NOTIMPL; } STDMETHODIMP HardDisk2::CloneTo (IHardDisk2 *aTarget, IProgress **aProgress) { AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); return E_NOTIMPL; } STDMETHODIMP HardDisk2::FlattenTo (IHardDisk2 *aTarget, IProgress **aProgress) { AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); return E_NOTIMPL; } // public methods for internal purposes only //////////////////////////////////////////////////////////////////////////////// /** * Checks if the given change of \a aOldPath to \a aNewPath affects the location * of this hard disk or any its child and updates the paths if necessary to * reflect the new location. * * @param aOldPath Old path (full). * @param aNewPath New path (full). * * @note Locks this object and all children for writing. */ void HardDisk2::updatePaths (const char *aOldPath, const char *aNewPath) { AssertReturnVoid (aOldPath); AssertReturnVoid (aNewPath); AutoCaller autoCaller (this); AssertComRCReturnVoid (autoCaller.rc()); AutoWriteLock alock (this); updatePath (aOldPath, aNewPath); /* update paths of all children */ for (List::const_iterator it = children().begin(); it != children().end(); ++ it) { (*it)->updatePaths (aOldPath, aNewPath); } } /** * Returns the base hard disk of the hard disk chain this hard disk is part of. * * The root hard disk is found by walking up the parent-child relationship axis. * If the hard disk doesn't have a parent (i.e. it's a base hard disk), it * returns itself in response to this method. * * @param aLevel Where to store the number of ancestors of this hard disk * (zero for the root), may be @c NULL. * * @note This method may return an uninitialized object if it happens in * parallel with such an operation as the VirtualBox shutdown or hard disk * merge. * * @note Locks this object and ancestors for reading. Neither this object nor * its ancestors may be locked or have active callers on the current * thread otherwise a deadlock or an endless loop will occur. */ ComObjPtr HardDisk2::root (uint32_t *aLevel /*= NULL*/) { /* Note: This method locks hard disks sequentially to avoid breaking the * {parent,child} lock order which is impossible to follow here. As a * result, any hard disk in the chain may become uninitialized before this * method finishes walking (for example, as a result of the merge operation * which removes hard disks from the chain). For this reason, we retry the * walk from the beginnig each time it happens (because we must always * return a non-null object). * * As the worst case, we end up having ourselves uninitialized (either * because VirtualBox has initiated the shutdown procedure, or because we * were closed/deleted/etc). In this case, we just return ourselves to avoid * the endless loop. */ ComObjPtr root; uint32_t level; do { root = this; level = 0; do { AutoCaller autoCaller (root); if (!autoCaller.isOk()) { /* break the endless loop, see above */ if (root == this) break; /* cause to start over with this object */ root.setNull(); break; } AutoReadLock alock (root); if (root->mParent.isNull()) break; root = root->mParent; ++ level; } while (true); } while (root.isNull()); if (aLevel != NULL) *aLevel = level; return root; } /** * Returns @c true if this hard disk cannot be modified because it has * dependants (children) or is part of the snapshot. Related to the hard disk * type and posterity, not to the current media state. * * @note Locks this object for reading. */ bool HardDisk2::isReadOnly() { AutoCaller autoCaller (this); AssertComRCReturn (autoCaller.rc(), false); AutoReadLock alock (this); switch (mm.type) { case HardDiskType_Normal: { if (children().size() != 0) return true; for (BackRefList::const_iterator it = m.backRefs.begin(); it != m.backRefs.end(); ++ it) if (it->snapshotIds.size() != 0) return true; return false; } case HardDiskType_Immutable: { return true; } case HardDiskType_Writethrough: { return false; } default: break; } AssertFailedReturn (false); } /** * Saves hard disk data by appending a new child node to the given * parent node which can be either or . * * @param aaParentNode Parent or node. * * @note Locks this object for reading. */ HRESULT HardDisk2::saveSettings (settings::Key &aParentNode) { using namespace settings; AssertReturn (!aParentNode.isNull(), E_FAIL); AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); Key diskNode = aParentNode.appendKey ("HardDisk"); /* required */ diskNode.setValue ("uuid", m.id); /* required (note: the original locaiton, not full) */ diskNode.setValue ("location", m.location); /* required */ diskNode.setValue ("format", mm.format); /* optional */ if (!m.description.isNull()) { Key descNode = diskNode.createKey ("Description"); descNode.setKeyValue (m.description); } /* only for base hard disks */ if (mParent.isNull()) { const char *type = mm.type == HardDiskType_Normal ? "Normal" : mm.type == HardDiskType_Immutable ? "Immutable" : mm.type == HardDiskType_Writethrough ? "Writethrough" : NULL; Assert (type != NULL); diskNode.setStringValue ("type", type); } /* save all children */ for (List::const_iterator it = children().begin(); it != children().end(); ++ it) { HRESULT rc = (*it)->saveSettings (diskNode); AssertComRCReturnRC (rc); } return S_OK; } /** * Compares the location of this hard disk to the given location. * * The comparison takes the location details into account. For example, if the * location is a file in the host's filesystem, a case insensitive comparison * will be performed for case insensitive filesystems. * * @param aLocation Location to compare to (as is). * @param aResult Where to store the result of comparison: 0 if locations * are equal, 1 if this object's location is greater than * the specified location, and -1 otherwise. */ HRESULT HardDisk2::compareLocationTo (const char *aLocation, int &aResult) { AutoCaller autoCaller (this); AssertComRCReturnRC (autoCaller.rc()); AutoReadLock alock (this); Utf8Str locationFull (m.locationFull); /// @todo NEWMEDIA delegate the comparison to the backend? if (isFileLocation (locationFull)) { Utf8Str location (aLocation); /* For locations represented by files, append the default path if * only the name is given, and then get the full path. */ if (!RTPathHavePath (aLocation)) { AutoReadLock propsLock (mVirtualBox->systemProperties()); location = Utf8StrFmt ("%ls%c%s", mVirtualBox->systemProperties()->defaultHardDiskFolder().raw(), RTPATH_DELIMITER, aLocation); } int vrc = mVirtualBox->calculateFullPath (location, location); if (RT_FAILURE (vrc)) return setError (E_FAIL, tr ("Invalid hard disk storage file location '%s' (%Rrc)"), location.raw(), vrc); aResult = RTPathCompare (locationFull, location); } else aResult = locationFull.compare (aLocation); return S_OK; } /** * Returns a short version of the location attribute. * * Reimplements MediumBase::name() to specially treat non-FS-path locations. * * @note Must be called from under this object's read or write lock. */ Utf8Str HardDisk2::name() { /// @todo NEWMEDIA treat non-FS-paths specially! (may require to requiest /// this information from the VD backend) Utf8Str location (m.locationFull); Utf8Str name = RTPathFilename (location); return name; } /** * Returns @c true if the given location is a file in the host's filesystem. * * @param aLocation Location to check. */ /*static*/ bool HardDisk2::isFileLocation (const char *aLocation) { /// @todo NEWMEDIA need some library that deals with URLs in a generic way if (aLocation == NULL) return false; size_t len = strlen (aLocation); /* unix-like paths */ if (len >= 1 && RTPATH_IS_SLASH (aLocation [0])) return true; /* dos-like paths */ if (len >= 2 && RTPATH_IS_VOLSEP (aLocation [1]) && ((aLocation [0] >= 'A' && aLocation [0] <= 'Z') || (aLocation [0] >= 'a' && aLocation [0] <= 'z'))) return true; /* if there is a ':' suffux which is not 'file:', return false */ const char *s = strchr (aLocation, ':'); if (s != NULL) { if (s - aLocation != 4) return false; if ((aLocation [0] != 'f' && aLocation [0] == 'F') || (aLocation [1] != 'i' && aLocation [1] == 'I') || (aLocation [2] != 'l' && aLocation [2] == 'L') || (aLocation [3] != 'e' && aLocation [3] == 'E')) return false; } /* everything else is treaten as file */ return true; } /** * Checks that this hard disk may be discarded and performs necessary state * changes. * * This method is to be called prior to calling the #discrad() to perform * necessary consistency checks and place involved hard disks to appropriate * states. If #discard() is not called or fails, the state modifications * performed by this method must be undone by #cancelDiscard(). * * See #discard() for more info about discarding hard disks. * * @param aChain Where to store the created merge chain (may return NULL * if no real merge is necessary). * * @note Locks the branch lock for reading. Locks this object, aTarget and all * intermediate hard disks for writing. */ HRESULT HardDisk2::prepareDiscard (MergeChain * &aChain) { AutoCaller autoCaller (this); AssertComRCReturnRC (autoCaller.rc()); aChain = NULL; AutoWriteLock alock (this); AssertReturn (mm.type == HardDiskType_Normal, E_FAIL); if (children().size() == 0) { /* special treatment of the last hard disk in the chain: */ if (mParent.isNull()) { /* lock only, to prevent any usage; discard() will unlock */ return LockWrite (NULL); } /* the differencing hard disk w/o children will be deleted, protect it * from attaching to other VMs (this is why Deleting) */ switch (m.state) { case MediaState_Created: m.state = MediaState_Deleting; break; default: return setStateError(); } /* aChain is intentionally NULL here */ return S_OK; } /* not going multi-merge as it's too expensive */ if (children().size() > 1) return setError (E_FAIL, tr ("Hard disk '%ls' has more than one child hard disk (%d)"), m.locationFull.raw(), children().size()); /* this is a read-only hard disk with children; it must be associated with * exactly one snapshot (when the snapshot is being taken, none of the * current VM's hard disks may be attached to other VMs). Note that by the * time when discard() is called, there must be no any attachments at all * (the code calling prepareDiscard() should detach). */ AssertReturn (m.backRefs.size() == 1 && !m.backRefs.front().inCurState && m.backRefs.front().snapshotIds.size() == 1, E_FAIL); ComObjPtr child = children().front(); /* we keep this locked, so lock the affected child to make sure the lock * order is correct when calling prepareMergeTo() */ AutoWriteLock childLock (child); /* delegate the rest to the profi */ if (mParent.isNull()) { /* base hard disk, backward merge */ Assert (child->m.backRefs.size() == 1); if (child->m.backRefs.front().machineId != m.backRefs.front().machineId) { /* backward merge is too tricky, we'll just detach on discard, so * lock only, to prevent any usage; discard() will only unlock * (since we return NULL in aChain) */ return LockWrite (NULL); } return child->prepareMergeTo (this, aChain, true /* aIgnoreAttachments */); } else { /* forward merge */ return prepareMergeTo (child, aChain, true /* aIgnoreAttachments */); } } /** * Discards this hard disk. * * Discarding the hard disk is merging its contents to its differencing child * hard disk (forward merge) or contents of its child hard disk to itself * (backward merge) if this hard disk is a base hard disk. If this hard disk is * a differencing hard disk w/o children, then it will be simply deleted. * Calling this method on a base hard disk w/o children will do nothing and * silently succeed. If this hard disk has more than one child, the method will * currently return an error (since merging in this case would be too expensive * and result in data duplication). * * When the backward merge takes place (i.e. this hard disk is a target) then, * on success, this hard disk will automatically replace the differencing child * hard disk used as a source (which will then be deleted) in the attachment * this child hard disk is associated with. This will happen only if both hard * disks belong to the same machine because otherwise such a replace would be * too tricky and could be not expected by the other machine. Same relates to a * case when the child hard disk is not associated with any machine at all. When * the backward merge is not applied, the method behaves as if the base hard * disk were not attached at all -- i.e. simply detaches it from the machine but * leaves the hard disk chain intact. * * This method is basically a wrapper around #mergeTo() that selects the correct * merge direction and performs additional actions as described above and. * * Note that this method will not return until the merge operation is complete * (which may be quite time consuming depending on the size of the merged hard * disks). * * Note that #prepareDiscard() must be called before calling this method. If * this method returns a failure, the caller must call #cancelDiscard(). On * success, #cancelDiscard() must not be called (this method will perform all * necessary steps such as resetting states of all involved hard disks and * deleting @a aChain). * * @param aChain Merge chain created by #prepareDiscard() (may be NULL if * no real merge takes place). * * @note Locks the branch lock for writing. Locks the hard disks from the chain * for writing. Locks the machine object when the backward merge takes * place. */ HRESULT HardDisk2::discard (ComObjPtr &aProgress, MergeChain *aChain) { AssertReturn (!aProgress.isNull(), E_FAIL); ComObjPtr hdFrom; HRESULT rc = S_OK; { AutoCaller autoCaller (this); AssertComRCReturnRC (autoCaller.rc()); aProgress->advanceOperation (BstrFmt ( tr ("Discarding hard disk '%s'"), name().raw())); if (aChain == NULL) { AutoWriteLock alock (this); Assert (children().size() == 0); /* special treatment of the last hard disk in the chain: */ if (mParent.isNull()) { rc = UnlockWrite (NULL); AssertComRC (rc); return rc; } /* delete the differencing hard disk w/o children */ Assert (m.state == MediaState_Deleting); /* go back to Created since deleteStorage() expects this state */ m.state = MediaState_Created; hdFrom = this; rc = deleteStorageAndWait (&aProgress); } else { hdFrom = aChain->source(); rc = hdFrom->mergeToAndWait (aChain, &aProgress); } } if (SUCCEEDED (rc)) { /* mergeToAndWait() cannot uninitialize the initiator because of * possible AutoCallers on the current thread, deleteStorageAndWait() * doesn't do it either; do it ourselves */ hdFrom->uninit(); } return rc; } /** * Undoes what #prepareDiscard() did. Must be called if #discard() is not called * or fails. Frees memory occupied by @a aChain. * * @param aChain Merge chain created by #prepareDiscard() (may be NULL if * no real merge takes place). * * @note Locks the hard disks from the chain for writing. */ void HardDisk2::cancelDiscard (MergeChain *aChain) { AutoCaller autoCaller (this); AssertComRCReturnVoid (autoCaller.rc()); if (aChain == NULL) { AutoWriteLock alock (this); Assert (children().size() == 0); /* special treatment of the last hard disk in the chain: */ if (mParent.isNull()) { HRESULT rc = UnlockWrite (NULL); AssertComRC (rc); return; } /* the differencing hard disk w/o children will be deleted, protect it * from attaching to other VMs (this is why Deleting) */ Assert (m.state == MediaState_Deleting); m.state = MediaState_Created; return; } /* delegate the rest to the profi */ cancelMergeTo (aChain); } // protected methods //////////////////////////////////////////////////////////////////////////////// /** * Deletes the hard disk storage unit. * * If @a aProgress is not NULL but the object it points to is @c null then a new * progress object will be created and assigned to @a *aProgress on success, * otherwise the existing progress object is used. If Progress is NULL, then no * progress object is created/used at all. * * When @a aWait is @c false, this method will create a thread to perform the * delete operation asynchronously and will return immediately. Otherwise, it * will perform the operation on the calling thread and will not return to the * caller until the operation is completed. Note that @a aProgress cannot be * NULL when @a aWait is @c false (this method will assert in this case). * * @param aProgress Where to find/store a Progress object to track operation * completion. * @param aWait @c true if this method should block instead of creating * an asynchronous thread. * * @note Locks mVirtualBox, mParent and this object for writing. */ HRESULT HardDisk2::deleteStorage (ComObjPtr *aProgress, bool aWait) { AssertReturn (aProgress != NULL || aWait == true, E_FAIL); AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); /* unregisterWithVirtualBox() needs a write lock. We want to unregister * ourselves atomically after detecting that deletion is possible to make * sure that we don't do that after another thread has done * VirtualBox::findHardDisk2() but before it starts using us (provided that * it holds a mVirtualBox lock too of course). * * mParent->removeDependentChild() needs a write lock too. */ AutoMultiWriteLock3 alock (mVirtualBox, mParent, this); switch (m.state) { case MediaState_Created: break; default: return setStateError(); } if (m.backRefs.size() != 0) return setError (E_FAIL, tr ("Hard disk '%ls' is attached to %d virtual machines"), m.locationFull.raw(), m.backRefs.size()); HRESULT rc = canClose(); CheckComRCReturnRC (rc); /* try to remove from the list of known hard disks before performing actual * deletion (we favor the consistency of the media registry in the first * place which would have been broken if unregisterWithVirtualBox() failed * after we successfully deleted the storage) */ rc = unregisterWithVirtualBox(); CheckComRCReturnRC (rc); ComObjPtr progress; if (aProgress != NULL) { /* use the existing progress object... */ progress = *aProgress; /* ...but create a new one if it is null */ if (progress.isNull()) { progress.createObject(); rc = progress->init (mVirtualBox, static_cast (this), BstrFmt (tr ("Deleting hard disk storage unit '%ls'"), name().raw()), FALSE /* aCancelable */); CheckComRCReturnRC (rc); } } std::auto_ptr task (new Task (this, progress, Task::Delete)); AssertComRCReturnRC (task->autoCaller.rc()); if (aWait) { /* go to Deleting state before starting the task */ m.state = MediaState_Deleting; rc = task->runNow(); } else { rc = task->startThread(); CheckComRCReturnRC (rc); /* go to Deleting state before leaving the lock */ m.state = MediaState_Deleting; } /* task is now owned (or already deleted) by taskThread() so release it */ task.release(); if (aProgress != NULL) { /* return progress to the caller */ *aProgress = progress; } return rc; } /** * Creates a new differencing storage unit using the given target hard disk's * format and the location. Note that @c aTarget must be NotCreated. * * As opposed to the CreateDiffStorage() method, this method doesn't try to lock * this hard disk for reading assuming that the caller has already done so. This * is used when taking an online snaopshot (where all origial hard disks are * locked for writing and must remain such). Note however that if @a aWait is * @c false and this method returns a success then the thread started by * this method will* unlock the hard disk (unless it is in * MediaState_LockedWrite state) so make sure the hard disk is either in * MediaState_LockedWrite or call #LockRead() before calling this method! If @a * aWait is @c true then this method neither locks nor unlocks the hard disk, so * make sure you do it yourself as needed. * * If @a aProgress is not NULL but the object it points to is @c null then a new * progress object will be created and assigned to @a *aProgress on success, * otherwise the existing progress object is used. If Progress is NULL, then no * progress object is created/used at all. * * When @a aWait is @c false, this method will create a thread to perform the * create operation asynchronously and will return immediately. Otherwise, it * will perform the operation on the calling thread and will not return to the * caller until the operation is completed. Note that @a aProgress cannot be * NULL when @a aWait is @c false (this method will assert in this case). * * @param aTarget Target hard disk. * @param aProgress Where to find/store a Progress object to track operation * completion. * @param aWait @c true if this method should block instead of creating * an asynchronous thread. * * @note Locks this object and aTarget for writing. */ HRESULT HardDisk2::createDiffStorage (ComObjPtr &aTarget, ComObjPtr *aProgress, bool aWait) { AssertReturn (!aTarget.isNull(), E_FAIL); AssertReturn (aProgress != NULL || aWait == true, E_FAIL); AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); AutoCaller targetCaller (aTarget); CheckComRCReturnRC (targetCaller.rc()); AutoMultiWriteLock2 alock (this, aTarget); AssertReturn (mm.type != HardDiskType_Writethrough, E_FAIL); /* Note: MediaState_LockedWrite is ok when taking an online snapshot */ AssertReturn (m.state == MediaState_LockedRead || m.state == MediaState_LockedWrite, E_FAIL); if (aTarget->m.state != MediaState_NotCreated) return aTarget->setStateError(); HRESULT rc = S_OK; /* check that the hard disk is not attached to any VM in the current state*/ for (BackRefList::const_iterator it = m.backRefs.begin(); it != m.backRefs.end(); ++ it) { if (it->inCurState) { /* Note: when a VM snapshot is being taken, all normal hard disks * attached to the VM in the current state will be, as an exception, * also associated with the snapshot which is about to create (see * SnapshotMachine::init()) before deassociating them from the * current state (which takes place only on success in * Machine::fixupHardDisks2()), so that the size of snapshotIds * will be 1 in this case. The given condition is used to filter out * this legal situatinon and do not report an error. */ if (it->snapshotIds.size() == 0) { return setError (E_FAIL, tr ("Hard disk '%ls' is attached to a virtual machine " "with UUID {%RTuuid}. No differencing hard disks " "based on it may be created until it is detached"), m.location.raw(), it->machineId.raw()); } Assert (it->snapshotIds.size() == 1); } } ComObjPtr progress; if (aProgress != NULL) { /* use the existing progress object... */ progress = *aProgress; /* ...but create a new one if it is null */ if (progress.isNull()) { progress.createObject(); rc = progress->init (mVirtualBox, static_cast (this), BstrFmt (tr ("Creating differencing hard disk storage unit '%ls'"), aTarget->name().raw()), FALSE /* aCancelable */); CheckComRCReturnRC (rc); } } /* setup task object and thread to carry out the operation * asynchronously */ std::auto_ptr task (new Task (this, progress, Task::CreateDiff)); AssertComRCReturnRC (task->autoCaller.rc()); task->setData (aTarget); /* register a task (it will deregister itself when done) */ ++ mm.numCreateDiffTasks; Assert (mm.numCreateDiffTasks != 0); /* overflow? */ if (aWait) { /* go to Creating state before starting the task */ aTarget->m.state = MediaState_Creating; rc = task->runNow(); } else { rc = task->startThread(); CheckComRCReturnRC (rc); /* go to Creating state before leaving the lock */ aTarget->m.state = MediaState_Creating; } /* task is now owned (or already deleted) by taskThread() so release it */ task.release(); if (aProgress != NULL) { /* return progress to the caller */ *aProgress = progress; } return rc; } /** * Prepares this (source) hard disk, target hard disk and all intermediate hard * disks for the merge operation. * * This method is to be called prior to calling the #mergeTo() to perform * necessary consistency checks and place involved hard disks to appropriate * states. If #mergeTo() is not called or fails, the state modifications * performed by this method must be undone by #cancelMergeTo(). * * Note that when @a aIgnoreAttachments is @c true then it's the caller's * responsibility to detach the source and all intermediate hard disks before * calling #mergeTo() (which will fail otherwise). * * See #mergeTo() for more information about merging. * * @param aTarget Target hard disk. * @param aChain Where to store the created merge chain. * @param aIgnoreAttachments Don't check if the source or any intermediate * hard disk is attached to any VM. * * @note Locks the branch lock for reading. Locks this object, aTarget and all * intermediate hard disks for writing. */ HRESULT HardDisk2::prepareMergeTo (HardDisk2 *aTarget, MergeChain * &aChain, bool aIgnoreAttachments /*= false*/) { AssertReturn (aTarget != NULL, E_FAIL); AutoCaller autoCaller (this); AssertComRCReturnRC (autoCaller.rc()); AutoCaller targetCaller (this); AssertComRCReturnRC (targetCaller.rc()); aChain = NULL; /* tree lock is always the first */ AutoReadLock treeLock (this->treeLock()); HRESULT rc = S_OK; /* detect the merge direction */ bool forward; { HardDisk2 *parent = mParent; while (parent != NULL && parent != aTarget) parent = parent->mParent; if (parent == aTarget) forward = false; else { parent = aTarget->mParent; while (parent != NULL && parent != this) parent = parent->mParent; if (parent == this) forward = true; else { Bstr tgtLoc; rc = aTarget->COMGETTER(Location) (tgtLoc.asOutParam()); CheckComRCThrowRC (rc); AutoReadLock alock (this); return setError (E_FAIL, tr ("Hard disks '%ls' and '%ls' are unrelated"), m.locationFull.raw(), tgtLoc.raw()); } } } /* build the chain (will do necessary checks and state changes) */ std::auto_ptr chain (new MergeChain (forward, aIgnoreAttachments)); { HardDisk2 *last = forward ? aTarget : this; HardDisk2 *first = forward ? this : aTarget; for (;;) { if (last == aTarget) rc = chain->addTarget (last); else if (last == this) rc = chain->addSource (last); else rc = chain->addIntermediate (last); CheckComRCReturnRC (rc); if (last == first) break; last = last->mParent; } } aChain = chain.release(); return S_OK; } /** * Merges this hard disk to the specified hard disk which must be either its * direct ancestor or descendant. * * Given this hard disk is SOURCE and the specified hard disk is TARGET, we will * get two varians of the merge operation: * * forward merge * -------------------------> * [Extra] <- SOURCE <- Intermediate <- TARGET * Any Del Del LockWr * * * backward merge * <------------------------- * TARGET <- Intermediate <- SOURCE <- [Extra] * LockWr Del Del LockWr * * Each scheme shows the involved hard disks on the hard disk chain where * SOURCE and TARGET belong. Under each hard disk there is a state value which * the hard disk must have at a time of the mergeTo() call. * * The hard disks in the square braces may be absent (e.g. when the forward * operation takes place and SOURCE is the base hard disk, or when the backward * merge operation takes place and TARGET is the last child in the chain) but if * they present they are involved too as shown. * * Nor the source hard disk neither intermediate hard disks may be attached to * any VM directly or in the snapshot, otherwise this method will assert. * * The #prepareMergeTo() method must be called prior to this method to place all * involved to necessary states and perform other consistency checks. * * If @a aWait is @c true then this method will perform the operation on the * calling thread and will not return to the caller until the operation is * completed. When this method succeeds, all intermediate hard disk objects in * the chain will be uninitialized, the state of the target hard disk (and all * involved extra hard disks) will be restored and @a aChain will be deleted. * Note that this (source) hard disk is not uninitialized because of possible * AutoCaller instances held by the caller of this method on the current thread. * It's therefore the responsibility of the caller to call HardDisk2::uninit() * after releasing all callers in this case! * * If @a aWait is @c false then this method will crea,te a thread to perform the * create operation asynchronously and will return immediately. If the operation * succeeds, the thread will uninitialize the source hard disk object and all * intermediate hard disk objects in the chain, reset the state of the target * hard disk (and all involved extra hard disks) and delete @a aChain. If the * operation fails, the thread will only reset the states of all involved hard * disks and delete @a aChain. * * When this method fails (regardless of the @a aWait mode), it is a caller's * responsiblity to undo state changes and delete @a aChain using * #cancelMergeTo(). * * If @a aProgress is not NULL but the object it points to is @c null then a new * progress object will be created and assigned to @a *aProgress on success, * otherwise the existing progress object is used. If Progress is NULL, then no * progress object is created/used at all. Note that @a aProgress cannot be * NULL when @a aWait is @c false (this method will assert in this case). * * @param aChain Merge chain created by #prepareMergeTo(). * @param aProgress Where to find/store a Progress object to track operation * completion. * @param aWait @c true if this method should block instead of creating * an asynchronous thread. * * @note Locks the branch lock for writing. Locks the hard disks from the chain * for writing. */ HRESULT HardDisk2::mergeTo (MergeChain *aChain, ComObjPtr *aProgress, bool aWait) { AssertReturn (aChain != NULL, E_FAIL); AssertReturn (aProgress != NULL || aWait == true, E_FAIL); AutoCaller autoCaller (this); CheckComRCReturnRC (autoCaller.rc()); HRESULT rc = S_OK; ComObjPtr progress; if (aProgress != NULL) { /* use the existing progress object... */ progress = *aProgress; /* ...but create a new one if it is null */ if (progress.isNull()) { AutoReadLock alock (this); progress.createObject(); rc = progress->init (mVirtualBox, static_cast (this), BstrFmt (tr ("Merging hard disk '%ls' to '%ls'"), name().raw(), aChain->target()->name().raw()), FALSE /* aCancelable */); CheckComRCReturnRC (rc); } } /* setup task object and thread to carry out the operation * asynchronously */ std::auto_ptr task (new Task (this, progress, Task::Merge)); AssertComRCReturnRC (task->autoCaller.rc()); task->setData (aChain); /* Note: task owns aChain (will delete it when not needed) in all cases * except when @a aWait is @c true and runNow() fails -- in this case * aChain will be left away because cancelMergeTo() will be applied by the * caller on it as it is required in the documentation above */ if (aWait) { rc = task->runNow(); } else { rc = task->startThread(); CheckComRCReturnRC (rc); } /* task is now owned (or already deleted) by taskThread() so release it */ task.release(); if (aProgress != NULL) { /* return progress to the caller */ *aProgress = progress; } return rc; } /** * Undoes what #prepareMergeTo() did. Must be called if #mergeTo() is not called * or fails. Frees memory occupied by @a aChain. * * @param aChain Merge chain created by #prepareMergeTo(). * * @note Locks the hard disks from the chain for writing. */ void HardDisk2::cancelMergeTo (MergeChain *aChain) { AutoCaller autoCaller (this); AssertComRCReturnVoid (autoCaller.rc()); AssertReturnVoid (aChain != NULL); /* the destructor will do the thing */ delete aChain; } // private methods //////////////////////////////////////////////////////////////////////////////// /** * Sets the value of m.location and calculates the value of m.locationFull. * * Reimplements MediumBase::setLocation() to specially treat non-FS-path * locations and to prepend the default hard disk folder if the given location * string does not contain any path information at all. * * Also, if the specified location is a file path that ends with '/' then the * file name part will be generated by this method automatically in the format * '{}.' where is a fresh UUID that this method will generate * and assign to this medium, and is the default extension for this * medium's storage format. Note that this procedure requires the media state to * be NotCreated and will return a faiulre otherwise. * * @param aLocation Location of the storage unit. If the locaiton is a FS-path, * then it can be relative to the VirtualBox home directory. * * @note Must be called from under this object's write lock. */ HRESULT HardDisk2::setLocation (const BSTR aLocation) { if (aLocation == NULL) return E_INVALIDARG; /// @todo NEWMEDIA treat non-FS-paths specially! (may require to request /// this information from the VD backend) Utf8Str location (aLocation); Guid id; if (RTPathFilename (location) == NULL) { /* no file name is given (either an empty string or ends with a slash), * generate a new UUID + file name if the state allows this */ if (m.state == MediaState_NotCreated) { id.create(); /// @todo NEWMEDIA use the default extension for the given VD backend location = Utf8StrFmt ("%s{%RTuuid}.vdi", location.raw(), id.raw()); } else { /* it's an error to not have a file name :) */ return setError (E_FAIL, tr ("Hard disk storage file location '%s' does not contain " "a file name"), location.raw()); } } /* append the default folder if no path is given */ if (!RTPathHavePath (location)) { AutoReadLock propsLock (mVirtualBox->systemProperties()); location = Utf8StrFmt ("%ls%c%s", mVirtualBox->systemProperties()->defaultHardDiskFolder().raw(), RTPATH_DELIMITER, location.raw()); } /* get the full file name */ Utf8Str locationFull; int vrc = mVirtualBox->calculateFullPath (location, locationFull); if (RT_FAILURE (vrc)) return setError (E_FAIL, tr ("Invalid hard disk storage file location '%s' (%Rrc)"), location.raw(), vrc); m.location = location; m.locationFull = locationFull; /* assign a new UUID if we generated it */ if (!id.isEmpty()) unconst (m.id) = id; return S_OK; } /** * Queries information from the image file. * * As a result of this call, the accessibility state and data members such as * size and description will be updated with the current information. * * Reimplements MediumBase::queryInfo() to query hard disk information using the * VD backend interface. * * @note This method may block during a system I/O call that checks storage * accessibility. * * @note Locks this object for writing. */ HRESULT HardDisk2::queryInfo() { AutoWriteLock alock (this); AssertReturn (m.state == MediaState_Created || m.state == MediaState_Inaccessible || m.state == MediaState_LockedRead || m.state == MediaState_LockedWrite, E_FAIL); HRESULT rc = S_OK; int vrc = VINF_SUCCESS; /* check if a blocking queryInfo() call is in progress on some other thread, * and wait for it to finish if so instead of querying data ourselves */ if (m.queryInfoSem != NIL_RTSEMEVENTMULTI) { Assert (m.state == MediaState_LockedRead); ++ m.queryInfoCallers; alock.leave(); vrc = RTSemEventMultiWait (m.queryInfoSem, RT_INDEFINITE_WAIT); alock.enter(); -- m.queryInfoCallers; if (m.queryInfoCallers == 0) { /* last waiting caller deletes the semaphore */ RTSemEventMultiDestroy (m.queryInfoSem); m.queryInfoSem = NIL_RTSEMEVENTMULTI; } AssertRC (vrc); return S_OK; } /* lazily create a semaphore for possible callers */ vrc = RTSemEventMultiCreate (&m.queryInfoSem); ComAssertRCRet (vrc, E_FAIL); bool tempStateSet = false; if (m.state != MediaState_LockedRead && m.state != MediaState_LockedWrite) { /* Cause other methods to prevent any modifications before leaving the * lock. Note that clients will never see this temporary state change * since any COMGETTER(State) is (or will be) blocked until we finish * and restore the actual state. */ m.state = MediaState_LockedRead; tempStateSet = true; } /* leave the lock before a blocking operation */ alock.leave(); bool success = false; Utf8Str lastAccessError; try { Utf8Str location (m.locationFull); /* are we dealing with an unknown (just opened) image? */ bool isNew = mm.format.isNull(); if (isNew) { /* detect the backend from the storage unit */ char *backendName = NULL; vrc = VDGetFormat (location, &backendName); if (RT_FAILURE (vrc)) { lastAccessError = Utf8StrFmt ( tr ("Could not get the storage format of the hard disk " "'%ls'%s"), m.locationFull.raw(), vdError (vrc).raw()); throw S_OK; } ComAssertThrow (backendName != NULL, E_FAIL); unconst (mm.format) = backendName; RTStrFree (backendName); } PVBOXHDD hdd; vrc = VDCreate (mm.vdDiskIfaces, &hdd); ComAssertRCThrow (vrc, E_FAIL); try { vrc = VDOpen (hdd, Utf8Str (mm.format), location, VD_OPEN_FLAGS_READONLY | VD_OPEN_FLAGS_INFO, NULL); if (RT_FAILURE (vrc)) { lastAccessError = Utf8StrFmt ( tr ("Could not open the hard disk '%ls'%s"), m.locationFull.raw(), vdError (vrc).raw()); throw S_OK; } /* check the UUID */ RTUUID uuid; vrc = VDGetUuid (hdd, 0, &uuid); ComAssertRCThrow (vrc, E_FAIL); if (isNew) { unconst (m.id) = uuid; } else { if (m.id != uuid) { lastAccessError = Utf8StrFmt ( tr ("UUID {%RTuuid} of the hard disk '%ls' " "does not match the value {%RTuuid} stored in the " "media registry ('%ls')"), &uuid, m.locationFull.raw(), m.id.raw(), mVirtualBox->settingsFileName().raw()); throw S_OK; } } /* check the type */ VDIMAGETYPE type; vrc = VDGetImageType (hdd, 0, &type); ComAssertRCThrow (vrc, E_FAIL); if (type == VD_IMAGE_TYPE_DIFF) { vrc = VDGetParentUuid (hdd, 0, &uuid); ComAssertRCThrow (vrc, E_FAIL); if (isNew) { /* the parent must be known to us. Note that we freely * call locking methods of mVirtualBox and parent from the * write lock (breaking the {parent,child} lock order) * because there may be no concurrent access to the just * opened hard disk on ther threads yet (and init() will * fail if this method reporst MediaState_Inaccessible) */ Guid id = uuid; ComObjPtr parent; rc = mVirtualBox->findHardDisk2 (&id, NULL, false /* aSetError */, &parent); if (FAILED (rc)) { lastAccessError = Utf8StrFmt ( tr ("Parent hard disk with UUID {%RTuuid} of the " "hard disk '%ls' is not found in the media " "registry ('%ls')"), &uuid, m.locationFull.raw(), mVirtualBox->settingsFileName().raw()); throw S_OK; } /* associate with parent, deassociate from VirtualBox */ Assert (mParent.isNull()); mParent = parent; mParent->addDependentChild (this); mVirtualBox->removeDependentChild (this); } else { /* check that parent UUIDs match. Note that there's no need * for the parent's AutoCaller (our lifetime is bound to * it) */ if (mParent.isNull()) { lastAccessError = Utf8StrFmt ( tr ("Hard disk '%ls' is differencing but it is not " "associated with any parent hard disk in the " "media registry ('%ls')"), m.locationFull.raw(), mVirtualBox->settingsFileName().raw()); throw S_OK; } AutoReadLock parentLock (mParent); if (mParent->state() != MediaState_Inaccessible && mParent->id() != uuid) { lastAccessError = Utf8StrFmt ( tr ("Parent UUID {%RTuuid} of the hard disk '%ls' " "does not match UUID {%RTuuid} of its parent " "hard disk stored in the media registry ('%ls')"), &uuid, m.locationFull.raw(), mParent->id().raw(), mVirtualBox->settingsFileName().raw()); throw S_OK; } /// @todo NEWMEDIA what to do if the parent is not /// accessible while the diff is? Probably, nothing. The /// real code will detect the mismatch anyway. } } m.size = VDGetFileSize (hdd, 0); mm.logicalSize = VDGetSize (hdd, 0) / _1M; success = true; } catch (HRESULT aRC) { rc = aRC; } VDDestroy (hdd); } catch (HRESULT aRC) { rc = aRC; } alock.enter(); /* inform other callers if there are any */ if (m.queryInfoCallers > 0) { RTSemEventMultiSignal (m.queryInfoSem); } else { /* delete the semaphore ourselves */ RTSemEventMultiDestroy (m.queryInfoSem); m.queryInfoSem = NIL_RTSEMEVENTMULTI; } /* Restore the proper state when appropriate. Keep in mind that LockedRead * and LockedWrite are not transitable to Inaccessible. */ if (success) { if (tempStateSet) m.state = MediaState_Created; m.lastAccessError.setNull(); } else { if (tempStateSet) m.state = MediaState_Inaccessible; m.lastAccessError = lastAccessError; LogWarningFunc (("'%ls' is not accessible (error='%ls', " "rc=%Rhrc, vrc=%Rrc)\n", m.locationFull.raw(), m.lastAccessError.raw(), rc, vrc)); } return rc; } /** * @note Called from this object's AutoMayUninitSpan and from under mVirtualBox * write lock. */ HRESULT HardDisk2::canClose() { if (children().size() != 0) return setError (E_FAIL, tr ("Hard disk '%ls' has %d child hard disks"), children().size()); return S_OK; } /** * @note Called from within this object's AutoWriteLock. */ HRESULT HardDisk2::canAttach (const Guid &aMachineId, const Guid &aSnapshotId) { if (mm.numCreateDiffTasks > 0) return setError (E_FAIL, tr ("One or more differencing child hard disks are " "being created for the hard disk '%ls' (%u)"), m.locationFull.raw(), mm.numCreateDiffTasks); return S_OK; } /** * @note Called from within this object's AutoMayUninitSpan (or AutoCaller) and * from under mVirtualBox write lock. * * @note Locks mParent for writing. */ HRESULT HardDisk2::unregisterWithVirtualBox() { /* Note that we need to de-associate ourselves from the parent to let * unregisterHardDisk2() properly save the registry */ const ComObjPtr parent = mParent; /* Lock parent to make the try atomic WRT to mParent->COMGETTER(Children) */ AutoWriteLock parentLock (parent); AssertReturn (children().size() == 0, E_FAIL); if (!mParent.isNull()) { /* deassociate from the parent, associate with VirtualBox */ mVirtualBox->addDependentChild (this); mParent->removeDependentChild (this); mParent.setNull(); } HRESULT rc = mVirtualBox->unregisterHardDisk2 (this); if (FAILED (rc)) { if (!parent.isNull()) { /* re-associate with the parent as we are still relatives in the * registry */ mParent = parent; mParent->addDependentChild (this); mVirtualBox->removeDependentChild (this); } } return rc; } /** * Returns the last error message collected by the vdErrorCall callback and * resets it. * * The error message is returned prepended with a dot and a space, like this: * * ". (%Rrc)" * * to make it easily appendable to a more general error message. The @c %Rrc * format string is given @a aVRC as an argument. * * If there is no last error message collected by vdErrorCall or if it is a * null or empty string, then this function returns the following text: * * " (%Rrc)" * * * @note Doesn't do any object locking; it is assumed that the caller makes sure * the callback isn't called by more than one thread at a time. * * @param aVRC VBox error code to use when no error message is provided. */ Utf8Str HardDisk2::vdError (int aVRC) { Utf8Str error; if (mm.vdError.isEmpty()) error = Utf8StrFmt (" (%Rrc)", aVRC); else error = Utf8StrFmt (". %s (%Rrc)", mm.vdError.raw(), aVRC); mm.vdError.setNull(); return error; } /** * Error message callback. * * Puts the reported error message to the mm.vdError field. * * @note Doesn't do any object locking; it is assumed that the caller makes sure * the callback isn't called by more than one thread at a time. * * @param pvUser The opaque data passed on container creation. * @param rc The VBox error code. * @param RT_SRC_POS_DECL Use RT_SRC_POS. * @param pszFormat Error message format string. * @param va Error message arguments. */ /*static*/ DECLCALLBACK(void) HardDisk2::vdErrorCall (void *pvUser, int rc, RT_SRC_POS_DECL, const char *pszFormat, va_list va) { HardDisk2 *that = static_cast (pvUser); AssertReturnVoid (that != NULL); that->mm.vdError = Utf8StrFmtVA (pszFormat, va); } /** * PFNVMPROGRESS callback handler for Task operations. * * @param uPercent Completetion precentage (0-100). * @param pvUser Pointer to the Progress instance. */ /*static*/ DECLCALLBACK(int) HardDisk2::vdProgressCall (PVM /* pVM */, unsigned uPercent, void *pvUser) { HardDisk2 *that = static_cast (pvUser); AssertReturn (that != NULL, VERR_GENERAL_FAILURE); if (that->mm.vdProgress != NULL) { /* update the progress object, capping it at 99% as the final percent * is used for additional operations like setting the UUIDs and similar. */ that->mm.vdProgress->notifyProgress (RT_MIN (uPercent, 99)); } return VINF_SUCCESS; } /** * Thread function for time-consuming tasks. * * The Task structure passed to @a pvUser must be allocated using new and will * be freed by this method before it returns. * * @param pvUser Pointer to the Task instance. */ /* static */ DECLCALLBACK(int) HardDisk2::taskThread (RTTHREAD thread, void *pvUser) { std::auto_ptr task (static_cast (pvUser)); AssertReturn (task.get(), VERR_GENERAL_FAILURE); bool isAsync = thread != NIL_RTTHREAD; HardDisk2 *that = task->that; /// @todo ugly hack, fix ComAssert... later #define setError that->setError /* Note: no need in AutoCaller because Task does that */ LogFlowFuncEnter(); LogFlowFunc (("{%p}: operation=%d\n", that, task->operation)); HRESULT rc = S_OK; switch (task->operation) { //////////////////////////////////////////////////////////////////////// case Task::CreateDynamic: case Task::CreateFixed: { /* The lock is also used as a signal from the task initiator (which * releases it only after RTThreadCreate()) that we can start the job */ AutoWriteLock thatLock (that); /* these parameters we need after creation */ RTUUID uuid; uint64_t size, logicalSize; try { PVBOXHDD hdd; int vrc = VDCreate (that->mm.vdDiskIfaces, &hdd); ComAssertRCThrow (vrc, E_FAIL); Utf8Str format (that->mm.format); Utf8Str location (that->m.locationFull); /* unlock before the potentially lengthy operation */ Assert (that->m.state == MediaState_Creating); thatLock.leave(); try { /* ensure the directory exists */ rc = VirtualBox::ensureFilePathExists (location); CheckComRCThrowRC (rc); PDMMEDIAGEOMETRY geo = { 0 }; /* auto-detect */ /* needed for vdProgressCallback */ that->mm.vdProgress = task->progress; vrc = VDCreateBase (hdd, format, location, task->operation == Task::CreateDynamic ? VD_IMAGE_TYPE_NORMAL : VD_IMAGE_TYPE_FIXED, task->d.size * _1M, VD_IMAGE_FLAGS_NONE, NULL, &geo, &geo, NULL, VD_OPEN_FLAGS_NORMAL, NULL, that->mm.vdDiskIfaces); if (RT_FAILURE (vrc)) { throw setError (E_FAIL, tr ("Could not create the hard disk storage " "unit '%s'%s"), location.raw(), that->vdError (vrc).raw()); } vrc = VDGetUuid (hdd, 0, &uuid); ComAssertRCThrow (vrc, E_FAIL); size = VDGetFileSize (hdd, 0); logicalSize = VDGetSize (hdd, 0) / _1M; } catch (HRESULT aRC) { rc = aRC; } VDDestroy (hdd); } catch (HRESULT aRC) { rc = aRC; } if (SUCCEEDED (rc)) { /* mVirtualBox->registerHardDisk2() needs a write lock */ AutoWriteLock vboxLock (that->mVirtualBox); thatLock.enter(); unconst (that->m.id) = uuid; that->m.size = size; that->mm.logicalSize = logicalSize; /* register with mVirtualBox as the last step and move to * Created state only on success (leaving an orphan file is * better than breaking media registry consistency) */ rc = that->mVirtualBox->registerHardDisk2 (that); if (SUCCEEDED (rc)) that->m.state = MediaState_Created; } if (FAILED (rc)) { thatLock.maybeEnter(); /* back to NotCreated on failiure */ that->m.state = MediaState_NotCreated; } break; } //////////////////////////////////////////////////////////////////////// case Task::CreateDiff: { ComObjPtr &target = task->d.target; /* Lock both in {parent,child} order. The lock is also used as a * signal from the task initiator (which releases it only after * RTThreadCreate()) that we can start the job*/ AutoMultiWriteLock2 thatLock (that, target); uint64_t size, logicalSize; try { PVBOXHDD hdd; int vrc = VDCreate (that->mm.vdDiskIfaces, &hdd); ComAssertRCThrow (vrc, E_FAIL); Utf8Str format (that->mm.format); Utf8Str location (that->m.locationFull); Utf8Str targetFormat (target->mm.format); Utf8Str targetLocation (target->m.locationFull); Guid targetId = target->m.id; /* UUID must have been set by setLocation() */ Assert (!targetId.isEmpty()); Assert (target->m.state == MediaState_Creating); /* Note: MediaState_LockedWrite is ok when taking an online * snapshot */ Assert (that->m.state == MediaState_LockedRead || that->m.state == MediaState_LockedWrite); /* unlock before the potentially lengthy operation */ thatLock.leave(); try { vrc = VDOpen (hdd, format, location, VD_OPEN_FLAGS_READONLY | VD_OPEN_FLAGS_INFO, NULL); if (RT_FAILURE (vrc)) { throw setError (E_FAIL, tr ("Could not open the hard disk storage " "unit '%s'%s"), location.raw(), that->vdError (vrc).raw()); } /* ensure the target directory exists */ rc = VirtualBox::ensureFilePathExists (targetLocation); CheckComRCThrowRC (rc); /* needed for vdProgressCallback */ that->mm.vdProgress = task->progress; vrc = VDCreateDiff (hdd, targetFormat, targetLocation, VD_IMAGE_FLAGS_NONE, NULL, targetId.raw(), VD_OPEN_FLAGS_NORMAL, NULL, that->mm.vdDiskIfaces); that->mm.vdProgress = NULL; if (RT_FAILURE (vrc)) { throw setError (E_FAIL, tr ("Could not create the differencing hard disk " "storage unit '%s'%s"), targetLocation.raw(), that->vdError (vrc).raw()); } size = VDGetFileSize (hdd, 0); logicalSize = VDGetSize (hdd, 0) / _1M; } catch (HRESULT aRC) { rc = aRC; } VDDestroy (hdd); } catch (HRESULT aRC) { rc = aRC; } if (SUCCEEDED (rc)) { /* mVirtualBox->registerHardDisk2() needs a write lock */ AutoWriteLock vboxLock (that->mVirtualBox); thatLock.enter(); target->m.size = size; target->mm.logicalSize = logicalSize; Assert (target->mParent.isNull()); /* associate the child with the parent and deassociate from * VirtualBox */ target->mParent = that; that->addDependentChild (target); target->mVirtualBox->removeDependentChild (target); /* register with mVirtualBox as the last step and move to * Created state only on success (leaving an orphan file is * better than breaking media registry consistency) */ rc = that->mVirtualBox->registerHardDisk2 (target); if (SUCCEEDED (rc)) { target->m.state = MediaState_Created; } else { /* break the parent association on failure to register */ target->mVirtualBox->addDependentChild (target); that->removeDependentChild (target); target->mParent.setNull(); } } if (FAILED (rc)) { thatLock.maybeEnter(); /* back to NotCreated on failiure */ target->m.state = MediaState_NotCreated; } if (isAsync) { /* unlock ourselves when done (unless in MediaState_LockedWrite * state because of taking the online snapshot*/ if (that->m.state != MediaState_LockedWrite) { HRESULT rc2 = that->UnlockRead (NULL); AssertComRC (rc2); } } /* deregister the task registered in createDiffStorage() */ Assert (that->mm.numCreateDiffTasks != 0); -- that->mm.numCreateDiffTasks; /* Note that in sync mode, it's the caller's responsibility to * unlock the hard disk */ break; } //////////////////////////////////////////////////////////////////////// case Task::Merge: { /* The lock is also used as a signal from the task initiator (which * releases it only after RTThreadCreate()) that we can start the * job. We don't actually need the lock for anything else since the * object is protected by MediaState_Deleting and we don't modify * its sensitive fields below */ { AutoWriteLock thatLock (that); } MergeChain *chain = task->d.chain.get(); #if 1 LogFlow (("*** MERGE forward = %RTbool\n", chain->isForward())); #endif try { PVBOXHDD hdd; int vrc = VDCreate (that->mm.vdDiskIfaces, &hdd); ComAssertRCThrow (vrc, E_FAIL); try { /* open all hard disks in the chain (they are in the * {parent,child} order in there. Note that we don't lock * objects in this chain since they must be in states * (Deleting and LockedWrite) that prevent from chaning * their format and location fields from outside. */ for (MergeChain::const_iterator it = chain->begin(); it != chain->end(); ++ it) { /* complex sanity (sane complexity) */ Assert ((chain->isForward() && (*it != chain->back() && (*it)->m.state == MediaState_Deleting) || (*it == chain->back() && (*it)->m.state == MediaState_LockedWrite)) || (!chain->isForward() && (*it != chain->front() && (*it)->m.state == MediaState_Deleting) || (*it == chain->front() && (*it)->m.state == MediaState_LockedWrite))); Assert (*it == chain->target() || (*it)->m.backRefs.size() == 0); /* open the first image with VDOPEN_FLAGS_INFO because * it's not necessarily the base one */ vrc = VDOpen (hdd, Utf8Str ((*it)->mm.format), Utf8Str ((*it)->m.locationFull), it == chain->begin() ? VD_OPEN_FLAGS_INFO : 0, NULL); if (RT_FAILURE (vrc)) throw vrc; #if 1 LogFlow (("*** MERGE disk = %ls\n", (*it)->m.locationFull.raw())); #endif } /* needed for vdProgressCallback */ that->mm.vdProgress = task->progress; unsigned start = chain->isForward() ? 0 : chain->size() - 1; unsigned end = chain->isForward() ? chain->size() - 1 : 0; #if 1 LogFlow (("*** MERGE from %d to %d\n", start, end)); #endif vrc = VDMerge (hdd, start, end, that->mm.vdDiskIfaces); that->mm.vdProgress = NULL; if (RT_FAILURE (vrc)) throw vrc; /* update parent UUIDs */ /// @todo VDMerge should be taught to do so, including the /// multiple children case if (chain->isForward()) { /* target's UUID needs to be updated (note that target * is the only image in the container on success) */ vrc = VDSetParentUuid (hdd, 0, chain->parent()->m.id); if (RT_FAILURE (vrc)) throw vrc; } else { /* we need to update UUIDs of all source's children * which cannot be part of the container at once so * add each one in there individually */ if (chain->children().size() > 0) { for (List::const_iterator it = chain->children().begin(); it != chain->children().end(); ++ it) { /* VD_OPEN_FLAGS_INFO since UUID is wrong yet */ vrc = VDOpen (hdd, Utf8Str ((*it)->mm.format), Utf8Str ((*it)->m.locationFull), VD_OPEN_FLAGS_INFO, NULL); if (RT_FAILURE (vrc)) throw vrc; vrc = VDSetParentUuid (hdd, 1, chain->target()->m.id); if (RT_FAILURE (vrc)) throw vrc; vrc = VDClose (hdd, false /* fDelete */); if (RT_FAILURE (vrc)) throw vrc; } } } } catch (HRESULT aRC) { rc = aRC; } catch (int aVRC) { throw setError (E_FAIL, tr ("Could not merge the hard disk '%ls' to '%ls'%s"), chain->source()->m.locationFull.raw(), chain->target()->m.locationFull.raw(), that->vdError (aVRC).raw()); } VDDestroy (hdd); } catch (HRESULT aRC) { rc = aRC; } HRESULT rc2; bool saveSettingsFailed = false; if (SUCCEEDED (rc)) { /* all hard disks but the target were successfully deleted by * VDMerge; reparent the last one and uninitialize deleted */ /* mVirtualBox->(un)registerHardDisk2() needs a write lock */ AutoWriteLock vboxLock (that->mVirtualBox); /* we will reparent */ AutoWriteLock treeLock (that->mVirtualBox->hardDiskTreeHandle()); HardDisk2 *source = chain->source(); HardDisk2 *target = chain->target(); if (chain->isForward()) { /* first, unregister the target since it may become a base * hard disk which needs re-registration */ rc2 = target->mVirtualBox-> unregisterHardDisk2 (target, false /* aSaveSettings */); AssertComRC (rc2); /* obey {parent,child} lock order (add/remove methods below * do locking) */ AutoWriteLock parentLock (chain->parent()); AutoWriteLock sourceLock (source); AutoWriteLock targetLock (target); /* then, reparent it and disconnect the deleted branch at * both ends (chain->parent() is source's parent) */ target->mParent->removeDependentChild (target); target->mParent = chain->parent(); if (!target->mParent.isNull()) { target->mParent->addDependentChild (target); target->mParent->removeDependentChild (source); source->mParent.setNull(); } else { target->mVirtualBox->addDependentChild (target); target->mVirtualBox->removeDependentChild (source); } /* then, register again */ rc2 = target->mVirtualBox-> registerHardDisk2 (target, false /* aSaveSettings */); AssertComRC (rc2); } else { /* obey {parent,child} lock order (add/remove methods below * do locking) */ AutoWriteLock targetLock (target); Assert (target->children().size() == 1); HardDisk2 *targetChild = target->children().front(); AutoWriteLock targetChildLock (target); /* disconnect the deleted branch at the elder end */ target->removeDependentChild (targetChild); targetChild->mParent.setNull(); const List &children = chain->children(); /* reparent source's chidren and disconnect the deleted * branch at the younger end m*/ if (children.size() > 0) { /* obey {parent,child} lock order */ AutoWriteLock sourceLock (source); for (List::const_iterator it = children.begin(); it != children.end(); ++ it) { AutoWriteLock childLock (*it); (*it)->mParent = target; (*it)->mParent->addDependentChild (*it); source->removeDependentChild (*it); } } } /* try to save the hard disk registry */ rc = that->mVirtualBox->saveSettings(); if (SUCCEEDED (rc)) { /* unregister and uninitialize all hard disks in the chain * but the target */ for (MergeChain::iterator it = chain->begin(); it != chain->end();) { if (*it == chain->target()) { ++ it; continue; } rc2 = (*it)->mVirtualBox-> unregisterHardDisk2 (*it, false /* aSaveSettings */); AssertComRC (rc2); /* 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) */ if (*it == that) task->autoCaller.release(); /* release the caller added by MergeChain before * uninit() */ (*it)->releaseCaller(); if (isAsync || *it != that) (*it)->uninit(); /* delete (to prevent uninitialization in MergeChain * dtor) and advance to the next item */ it = chain->erase (it); } /* Note that states of all other hard disks (target, parent, * children) will be restored by the MergeChain dtor */ } else { /* too bad if we fail, but we'll need to rollback everything * we did above to at least keep the the HD tree in sync * with the current regstry on disk */ saveSettingsFailed = true; /// @todo NEWMEDIA implement a proper undo AssertFailed(); } } if (FAILED (rc)) { /* Here we come if either VDMerge() failed (in which case we * assume that it tried to do everything to make a further * retry possible -- e.g. not deleted intermediate hard disks * and so on) or VirtualBox::saveSettings() failed (where we * should have the original tree but with intermediate storage * units deleted by VDMerge()). We have to only restore states * (through the MergeChain dtor) unless we are run syncronously * in which case it's the responsibility of the caller as stated * in the mergeTo() docs. The latter also implies that we * don't own the merge chain, so release it in this case. */ if (!isAsync) task->d.chain.release(); NOREF (saveSettingsFailed); } break; } //////////////////////////////////////////////////////////////////////// case Task::Delete: { /* The lock is also used as a signal from the task initiator (which * releases it only after RTThreadCreate()) that we can start the job */ AutoWriteLock thatLock (that); try { PVBOXHDD hdd; int vrc = VDCreate (that->mm.vdDiskIfaces, &hdd); ComAssertRCThrow (vrc, E_FAIL); Utf8Str format (that->mm.format); Utf8Str location (that->m.locationFull); /* unlock before the potentially lengthy operation */ Assert (that->m.state == MediaState_Deleting); thatLock.leave(); try { vrc = VDOpen (hdd, format, location, VD_OPEN_FLAGS_READONLY | VD_OPEN_FLAGS_INFO, NULL); if (RT_SUCCESS (vrc)) vrc = VDClose (hdd, true /* fDelete */); if (RT_FAILURE (vrc)) { throw setError (E_FAIL, tr ("Could not delete the hard disk storage " "unit '%s'%s"), location.raw(), that->vdError (vrc).raw()); } } catch (HRESULT aRC) { rc = aRC; } VDDestroy (hdd); } catch (HRESULT aRC) { rc = aRC; } thatLock.maybeEnter(); /* go to the NotCreated state even on failure since the storage * may have been already partially deleted and cannot be used any * more. One will be able to manually re-open the storage if really * needed to re-register it. */ that->m.state = MediaState_NotCreated; break; } default: AssertFailedReturn (VERR_GENERAL_FAILURE); } /* complete the progress if run asynchronously */ if (isAsync) { if (!task->progress.isNull()) task->progress->notifyComplete (rc); } else { task->rc = rc; } LogFlowFunc (("rc=%Rhrc\n", rc)); LogFlowFuncLeave(); return VINF_SUCCESS; /// @todo ugly hack, fix ComAssert... later #undef setError }