kpmcore/src/core/operationstack.cpp

572 lines
21 KiB
C++

/*************************************************************************
* Copyright (C) 2008, 2011 by Volker Lanz <vl@fidra.de> *
* Copyright (C) 2016 by Andrius Štikonas <andrius@stikonas.eu> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 3 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>.*
*************************************************************************/
#include "core/operationstack.h"
#include "core/device.h"
#include "core/partition.h"
#include "core/partitiontable.h"
#include "ops/operation.h"
#include "ops/deleteoperation.h"
#include "ops/newoperation.h"
#include "ops/resizeoperation.h"
#include "ops/copyoperation.h"
#include "ops/restoreoperation.h"
#include "ops/createfilesystemoperation.h"
#include "ops/setpartflagsoperation.h"
#include "ops/setfilesystemlabeloperation.h"
#include "ops/createpartitiontableoperation.h"
#include "ops/resizevolumegroupoperation.h"
#include "ops/checkoperation.h"
#include "jobs/setfilesystemlabeljob.h"
#include "fs/filesystemfactory.h"
#include "util/globallog.h"
#include <KLocalizedString>
#include <QReadLocker>
#include <QWriteLocker>
/** Constructs a new OperationStack */
OperationStack::OperationStack(QObject* parent) :
QObject(parent),
m_Operations(),
m_PreviewDevices(),
m_Lock(QReadWriteLock::Recursive)
{
}
/** Destructs an OperationStack, cleaning up Operations and Devices */
OperationStack::~OperationStack()
{
clearOperations();
clearDevices();
}
/** Tries to merge an existing NewOperation with a new Operation pushed on the OperationStack
There are several cases what might need to be done:
<ol>
<!-- 1 -->
<li>An existing operation created a Partition that is now being deleted: In this case, just remove
the corresponding NewOperation from the OperationStack.<br/>This does not work for
extended partitions.(#232092)</li>
<!-- 2 -->
<li>An existing Operation created a Partition that is now being moved or resized. In this case,
remove the original NewOperation and create a new NewOperation with updated start and end
sectors. This new NewOperation is appended to the OperationStack.<br/>This does not work for
extended partitions.(#232092)</li>
<!-- 3 -->
<li>An existing NewOperation created a Partition that is now being copied. We're not copying
but instead creating another new Partition in its place.</li>
<!-- 4 -->
<li>The label for a new Partition's FileSystem is modified: Modify in NewOperation and forget it.</li>
<!-- 5 -->
<li>File system is changed for a new Partition: Modify in NewOperation and forget it.</li>
<!-- 6 -->
<li>A file system on a new Partition is about to be checked: Just delete the CheckOperation, because
file systems are checked anyway when they're created. This fixes #275657.</li>
</ol>
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergeNewOperation(Operation*& currentOp, Operation*& pushedOp)
{
NewOperation* newOp = dynamic_cast<NewOperation*>(currentOp);
if (newOp == nullptr)
return false;
DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
ResizeOperation* pushedResizeOp = dynamic_cast<ResizeOperation*>(pushedOp);
CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
CreateFileSystemOperation* pushedCreateFileSystemOp = dynamic_cast<CreateFileSystemOperation*>(pushedOp);
CheckOperation* pushedCheckOp = dynamic_cast<CheckOperation*>(pushedOp);
// -- 1 --
if (pushedDeleteOp && &newOp->newPartition() == &pushedDeleteOp->deletedPartition() && !pushedDeleteOp->deletedPartition().roles().has(PartitionRole::Extended)) {
Log() << xi18nc("@info:status", "Deleting a partition just created: Undoing the operation to create the partition.");
delete pushedOp;
pushedOp = nullptr;
newOp->undo();
delete operations().takeAt(operations().indexOf(newOp));
return true;
}
// -- 2 --
if (pushedResizeOp && &newOp->newPartition() == &pushedResizeOp->partition() && !pushedResizeOp->partition().roles().has(PartitionRole::Extended)) {
// NOTE: In theory it would be possible to merge resizing an extended as long as it has no children.
// But that still doesn't save us: If we're not merging a resize on an extended that has children,
// a resizeop is added to the stack. Next, the user deletes the child. Then he resizes the
// extended again (a second resize): The ResizeOp still has the pointer to the original extended that
// will now be deleted.
Log() << xi18nc("@info:status", "Resizing a partition just created: Updating start and end in existing operation.");
Partition* newPartition = new Partition(newOp->newPartition());
newPartition->setFirstSector(pushedResizeOp->newFirstSector());
newPartition->fileSystem().setFirstSector(pushedResizeOp->newFirstSector());
newPartition->setLastSector(pushedResizeOp->newLastSector());
newPartition->fileSystem().setLastSector(pushedResizeOp->newLastSector());
NewOperation* revisedNewOp = new NewOperation(newOp->targetDevice(), newPartition);
delete pushedOp;
pushedOp = revisedNewOp;
newOp->undo();
delete operations().takeAt(operations().indexOf(newOp));
return true;
}
// -- 3 --
if (pushedCopyOp && &newOp->newPartition() == &pushedCopyOp->sourcePartition()) {
Log() << xi18nc("@info:status", "Copying a new partition: Creating a new partition instead.");
Partition* newPartition = new Partition(newOp->newPartition());
newPartition->setFirstSector(pushedCopyOp->copiedPartition().firstSector());
newPartition->fileSystem().setFirstSector(pushedCopyOp->copiedPartition().fileSystem().firstSector());
newPartition->setLastSector(pushedCopyOp->copiedPartition().lastSector());
newPartition->fileSystem().setLastSector(pushedCopyOp->copiedPartition().fileSystem().lastSector());
NewOperation* revisedNewOp = new NewOperation(pushedCopyOp->targetDevice(), newPartition);
delete pushedOp;
pushedOp = revisedNewOp;
return true;
}
// -- 4 --
if (pushedLabelOp && &newOp->newPartition() == &pushedLabelOp->labeledPartition()) {
Log() << xi18nc("@info:status", "Changing label for a new partition: No new operation required.");
newOp->setLabelJob()->setLabel(pushedLabelOp->newLabel());
newOp->newPartition().fileSystem().setLabel(pushedLabelOp->newLabel());
delete pushedOp;
pushedOp = nullptr;
return true;
}
// -- 5 --
if (pushedCreateFileSystemOp && &newOp->newPartition() == &pushedCreateFileSystemOp->partition()) {
Log() << xi18nc("@info:status", "Changing file system for a new partition: No new operation required.");
FileSystem* oldFs = &newOp->newPartition().fileSystem();
newOp->newPartition().setFileSystem(FileSystemFactory::cloneWithNewType(pushedCreateFileSystemOp->newFileSystem()->type(), *oldFs));
delete oldFs;
oldFs = nullptr;
delete pushedOp;
pushedOp = nullptr;
return true;
}
// -- 6 --
if (pushedCheckOp && &newOp->newPartition() == &pushedCheckOp->checkedPartition()) {
Log() << xi18nc("@info:status", "Checking file systems is automatically done when creating them: No new operation required.");
delete pushedOp;
pushedOp = nullptr;
return true;
}
return false;
}
/** Tries to merge an existing CopyOperation with a new Operation pushed on the OperationStack.
These are the cases to consider:
<ol>
<!-- 1 -->
<li>An existing CopyOperation created a Partition that is now being deleted. Remove the
CopyOperation, and, if the CopyOperation was an overwrite, carry on with the delete. Else
also remove the DeleteOperation.</li>
<!-- 2 -->
<li>An existing CopyOperation created a Partition that is now being copied. We're not copying
the target of this existing CopyOperation, but its source instead. If this merge is not done,
copied partitions will have misleading labels ("copy of sdXY" instead of "copy of copy of
sdXY" for a second-generation copy) but the Operation itself will still work.</li>
</ol>
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergeCopyOperation(Operation*& currentOp, Operation*& pushedOp)
{
CopyOperation* copyOp = dynamic_cast<CopyOperation*>(currentOp);
if (copyOp == nullptr)
return false;
DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
// -- 1 --
if (pushedDeleteOp && &copyOp->copiedPartition() == &pushedDeleteOp->deletedPartition()) {
// If the copy operation didn't overwrite, but create a new partition, just remove the
// copy operation, forget the delete and be done.
if (copyOp->overwrittenPartition() == nullptr) {
Log() << xi18nc("@info:status", "Deleting a partition just copied: Removing the copy.");
delete pushedOp;
pushedOp = nullptr;
} else {
Log() << xi18nc("@info:status", "Deleting a partition just copied over an existing partition: Removing the copy and deleting the existing partition.");
pushedDeleteOp->setDeletedPartition(copyOp->overwrittenPartition());
}
copyOp->undo();
delete operations().takeAt(operations().indexOf(copyOp));
return true;
}
// -- 2 --
if (pushedCopyOp && &copyOp->copiedPartition() == &pushedCopyOp->sourcePartition()) {
Log() << xi18nc("@info:status", "Copying a partition that is itself a copy: Copying the original source partition instead.");
pushedCopyOp->setSourcePartition(&copyOp->sourcePartition());
}
return false;
}
/** Tries to merge an existing RestoreOperation with a new Operation pushed on the OperationStack.
If an existing RestoreOperation created a Partition that is now being deleted, remove the
RestoreOperation, and, if the RestoreOperation was an overwrite, carry on with the delete. Else
also remove the DeleteOperation.
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergeRestoreOperation(Operation*& currentOp, Operation*& pushedOp)
{
RestoreOperation* restoreOp = dynamic_cast<RestoreOperation*>(currentOp);
if (restoreOp == nullptr)
return false;
DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
if (pushedDeleteOp && &restoreOp->restorePartition() == &pushedDeleteOp->deletedPartition()) {
if (restoreOp->overwrittenPartition() == nullptr) {
Log() << xi18nc("@info:status", "Deleting a partition just restored: Removing the restore operation.");
delete pushedOp;
pushedOp = nullptr;
} else {
Log() << xi18nc("@info:status", "Deleting a partition just restored to an existing partition: Removing the restore operation and deleting the existing partition.");
pushedDeleteOp->setDeletedPartition(restoreOp->overwrittenPartition());
}
restoreOp->undo();
delete operations().takeAt(operations().indexOf(restoreOp));
return true;
}
return false;
}
/** Tries to merge an existing SetPartFlagsOperation with a new Operation pushed on the OperationStack.
If the Partition flags for an existing Partition are modified look if there is an existing
Operation for the same Partition and modify that one.
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergePartFlagsOperation(Operation*& currentOp, Operation*& pushedOp)
{
SetPartFlagsOperation* partFlagsOp = dynamic_cast<SetPartFlagsOperation*>(currentOp);
if (partFlagsOp == nullptr)
return false;
SetPartFlagsOperation* pushedFlagsOp = dynamic_cast<SetPartFlagsOperation*>(pushedOp);
if (pushedFlagsOp && &partFlagsOp->flagPartition() == &pushedFlagsOp->flagPartition()) {
Log() << xi18nc("@info:status", "Changing flags again for the same partition: Removing old operation.");
pushedFlagsOp->setOldFlags(partFlagsOp->oldFlags());
partFlagsOp->undo();
delete operations().takeAt(operations().indexOf(partFlagsOp));
return true;
}
return false;
}
/** Tries to merge an existing SetFileSystemLabelOperation with a new Operation pushed on the OperationStack.
If a FileSystem label for an existing Partition is modified look if there is an existing
SetFileSystemLabelOperation for the same Partition.
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergePartLabelOperation(Operation*& currentOp, Operation*& pushedOp)
{
SetFileSystemLabelOperation* partLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(currentOp);
if (partLabelOp == nullptr)
return false;
SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
if (pushedLabelOp && &partLabelOp->labeledPartition() == &pushedLabelOp->labeledPartition()) {
Log() << xi18nc("@info:status", "Changing label again for the same partition: Removing old operation.");
pushedLabelOp->setOldLabel(partLabelOp->oldLabel());
partLabelOp->undo();
delete operations().takeAt(operations().indexOf(partLabelOp));
return true;
}
return false;
}
/** Tries to merge an existing CreatePartitionTableOperation with a new Operation pushed on the OperationStack.
If a new partition table is to be created on a device and a previous operation targets that
device, remove this previous operation.
@param currentOp the Operation already on the stack to try to merge with
@param pushedOp the newly pushed Operation
@return true if the OperationStack has been modified in a way that requires merging to stop
*/
bool OperationStack::mergeCreatePartitionTableOperation(Operation*& currentOp, Operation*& pushedOp)
{
CreatePartitionTableOperation* pushedCreatePartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(pushedOp);
if (pushedCreatePartitionTableOp && currentOp->targets(pushedCreatePartitionTableOp->targetDevice())) {
Log() << xi18nc("@info:status", "Creating new partition table, discarding previous operation on device.");
CreatePartitionTableOperation* createPartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(currentOp);
if (createPartitionTableOp != nullptr)
pushedCreatePartitionTableOp->setOldPartitionTable(createPartitionTableOp->oldPartitionTable());
currentOp->undo();
delete operations().takeAt(operations().indexOf(currentOp));
return true;
}
return false;
}
bool OperationStack::mergeResizeVolumeGroupResizeOperation(Operation*& pushedOp)
{
ResizeVolumeGroupOperation* pushedResizeVolumeGroupOp = dynamic_cast<ResizeVolumeGroupOperation*>(pushedOp);
if (pushedResizeVolumeGroupOp && pushedResizeVolumeGroupOp->jobs().count() == 0) {
Log() << xi18nc("@info:status", "Resizing Volume Group, nothing to do.");
return true;
}
return false;
}
/** Pushes a new Operation on the OperationStack.
This method will call all methods that try to merge the new Operation with the
existing ones. It is not uncommon that any of these will delete the pushed
Operation. Callers <b>must not rely</b> on the pushed Operation to exist after
calling OperationStack::push().
@param o Pointer to the Operation. Must not be nullptr.
*/
void OperationStack::push(Operation* o)
{
Q_ASSERT(o);
if (mergeResizeVolumeGroupResizeOperation(o))
return;
for (auto currentOp = operations().rbegin(); currentOp != operations().rend(); ++currentOp) {
if (mergeNewOperation(*currentOp, o))
break;
if (mergeCopyOperation(*currentOp, o))
break;
if (mergeRestoreOperation(*currentOp, o))
break;
if (mergePartFlagsOperation(*currentOp, o))
break;
if (mergePartLabelOperation(*currentOp, o))
break;
if (mergeCreatePartitionTableOperation(*currentOp, o))
break;
}
if (o != nullptr) {
Log() << xi18nc("@info:status", "Add operation: %1", o->description());
operations().append(o);
o->preview();
o->setStatus(Operation::StatusPending);
}
// emit operationsChanged even if o is nullptr because it has been merged: merging might
// have led to an existing operation changing.
emit operationsChanged();
}
/** Removes the topmost Operation from the OperationStack, calls Operation::undo() on it and deletes it. */
void OperationStack::pop()
{
Operation* o = operations().takeLast();
o->undo();
delete o;
emit operationsChanged();
}
/** Check whether previous operations involve given partition.
@param p Pointer to the Partition. Must not be nullptr.
*/
bool OperationStack::contains(const Partition* p) const
{
Q_ASSERT(p);
for (const auto &o : operations()) {
if (o->targets(*p))
return true;
CopyOperation* copyOp = dynamic_cast<CopyOperation*>(o);
if (copyOp) {
const Partition* source = &copyOp->sourcePartition();
if (source == p)
return true;
}
}
return false;
}
/** Removes all Operations from the OperationStack, calling Operation::undo() on them and deleting them. */
void OperationStack::clearOperations()
{
while (!operations().isEmpty()) {
Operation* o = operations().takeLast();
if (o->status() == Operation::StatusPending)
o->undo();
delete o;
}
emit operationsChanged();
}
/** Clears the list of Devices. */
void OperationStack::clearDevices()
{
QWriteLocker lockDevices(&lock());
qDeleteAll(previewDevices());
previewDevices().clear();
emit devicesChanged();
}
/** Finds a Device a Partition is on.
@param p pointer to the Partition to find a Device for
@return the Device or nullptr if none could be found
*/
Device* OperationStack::findDeviceForPartition(const Partition* p)
{
QReadLocker lockDevices(&lock());
const auto devices = previewDevices();
for (Device *d : devices) {
if (d->partitionTable() == nullptr)
continue;
const auto partitions = d->partitionTable()->children();
for (const auto *part : partitions) {
if (part == p)
return d;
for (const auto &child : part->children())
if (child == p)
return d;
}
}
return nullptr;
}
/** Adds a Device to the OperationStack
@param d pointer to the Device to add. Must not be nullptr.
*/
void OperationStack::addDevice(Device* d)
{
Q_ASSERT(d);
QWriteLocker lockDevices(&lock());
previewDevices().append(d);
emit devicesChanged();
}
static bool deviceLessThan(const Device* d1, const Device* d2)
{
// Display alphabetically sorted disk devices above LVM VGs
if (d1->type() == Device::LVM_Device && d2->type() == Device::Disk_Device )
return false;
return d1->deviceNode() <= d2->deviceNode();
}
void OperationStack::sortDevices()
{
QWriteLocker lockDevices(&lock());
std::sort(previewDevices().begin(), previewDevices().end(), deviceLessThan);
emit devicesChanged();
}