1 /*
2 SPDX-FileCopyrightText: 2008-2010 Volker Lanz <vl@fidra.de>
3 SPDX-FileCopyrightText: 2008 Laurent Montel <montel@kde.org>
4 SPDX-FileCopyrightText: 2014-2020 Andrius Štikonas <andrius@stikonas.eu>
5 SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
6
7 SPDX-License-Identifier: GPL-3.0-or-later
8 */
9
10 #include "core/operationstack.h"
11 #include "core/device.h"
12 #include "core/partition.h"
13 #include "core/partitiontable.h"
14
15 #include "ops/operation.h"
16 #include "ops/deleteoperation.h"
17 #include "ops/newoperation.h"
18 #include "ops/resizeoperation.h"
19 #include "ops/copyoperation.h"
20 #include "ops/restoreoperation.h"
21 #include "ops/createfilesystemoperation.h"
22 #include "ops/setpartflagsoperation.h"
23 #include "ops/setfilesystemlabeloperation.h"
24 #include "ops/createpartitiontableoperation.h"
25 #include "ops/resizevolumegroupoperation.h"
26 #include "ops/checkoperation.h"
27
28 #include "jobs/setfilesystemlabeljob.h"
29
30 #include "fs/filesystemfactory.h"
31
32 #include "util/globallog.h"
33
34 #include <KLocalizedString>
35
36 #include <QReadLocker>
37 #include <QWriteLocker>
38
39 /** Constructs a new OperationStack */
OperationStack(QObject * parent)40 OperationStack::OperationStack(QObject* parent) :
41 QObject(parent),
42 m_Operations(),
43 m_PreviewDevices(),
44 m_Lock(QReadWriteLock::Recursive)
45 {
46 }
47
48 /** Destructs an OperationStack, cleaning up Operations and Devices */
~OperationStack()49 OperationStack::~OperationStack()
50 {
51 clearOperations();
52 clearDevices();
53 }
54
55 /** Tries to merge an existing NewOperation with a new Operation pushed on the OperationStack
56
57 There are several cases what might need to be done:
58
59 <ol>
60 <!-- 1 -->
61 <li>An existing operation created a Partition that is now being deleted: In this case, just remove
62 the corresponding NewOperation from the OperationStack.<br/>This does not work for
63 extended partitions.(#232092)</li>
64 <!-- 2 -->
65 <li>An existing Operation created a Partition that is now being moved or resized. In this case,
66 remove the original NewOperation and create a new NewOperation with updated start and end
67 sectors. This new NewOperation is appended to the OperationStack.<br/>This does not work for
68 extended partitions.(#232092)</li>
69 <!-- 3 -->
70 <li>An existing NewOperation created a Partition that is now being copied. We're not copying
71 but instead creating another new Partition in its place.</li>
72 <!-- 4 -->
73 <li>The label for a new Partition's FileSystem is modified: Modify in NewOperation and forget it.</li>
74 <!-- 5 -->
75 <li>File system is changed for a new Partition: Modify in NewOperation and forget it.</li>
76 <!-- 6 -->
77 <li>A file system on a new Partition is about to be checked: Just delete the CheckOperation, because
78 file systems are checked anyway when they're created. This fixes #275657.</li>
79 </ol>
80
81 @param currentOp the Operation already on the stack to try to merge with
82 @param pushedOp the newly pushed Operation
83 @return true if the OperationStack has been modified in a way that requires merging to stop
84 */
mergeNewOperation(Operation * & currentOp,Operation * & pushedOp)85 bool OperationStack::mergeNewOperation(Operation*& currentOp, Operation*& pushedOp)
86 {
87 NewOperation* newOp = dynamic_cast<NewOperation*>(currentOp);
88
89 if (newOp == nullptr)
90 return false;
91
92 DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
93 ResizeOperation* pushedResizeOp = dynamic_cast<ResizeOperation*>(pushedOp);
94 CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
95 SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
96 CreateFileSystemOperation* pushedCreateFileSystemOp = dynamic_cast<CreateFileSystemOperation*>(pushedOp);
97 CheckOperation* pushedCheckOp = dynamic_cast<CheckOperation*>(pushedOp);
98
99 // -- 1 --
100 if (pushedDeleteOp && &newOp->newPartition() == &pushedDeleteOp->deletedPartition() && !pushedDeleteOp->deletedPartition().roles().has(PartitionRole::Extended)) {
101 Log() << xi18nc("@info:status", "Deleting a partition just created: Undoing the operation to create the partition.");
102
103 delete pushedOp;
104 pushedOp = nullptr;
105
106 newOp->undo();
107 delete operations().takeAt(operations().indexOf(newOp));
108
109 return true;
110 }
111
112 // -- 2 --
113 if (pushedResizeOp && &newOp->newPartition() == &pushedResizeOp->partition() && !pushedResizeOp->partition().roles().has(PartitionRole::Extended)) {
114 // NOTE: In theory it would be possible to merge resizing an extended as long as it has no children.
115 // But that still doesn't save us: If we're not merging a resize on an extended that has children,
116 // a resizeop is added to the stack. Next, the user deletes the child. Then he resizes the
117 // extended again (a second resize): The ResizeOp still has the pointer to the original extended that
118 // will now be deleted.
119 Log() << xi18nc("@info:status", "Resizing a partition just created: Updating start and end in existing operation.");
120
121 Partition* newPartition = new Partition(newOp->newPartition());
122 newPartition->setFirstSector(pushedResizeOp->newFirstSector());
123 newPartition->fileSystem().setFirstSector(pushedResizeOp->newFirstSector());
124 newPartition->setLastSector(pushedResizeOp->newLastSector());
125 newPartition->fileSystem().setLastSector(pushedResizeOp->newLastSector());
126
127 NewOperation* revisedNewOp = new NewOperation(newOp->targetDevice(), newPartition);
128 delete pushedOp;
129 pushedOp = revisedNewOp;
130
131 newOp->undo();
132 delete operations().takeAt(operations().indexOf(newOp));
133
134 return true;
135 }
136
137 // -- 3 --
138 if (pushedCopyOp && &newOp->newPartition() == &pushedCopyOp->sourcePartition()) {
139 Log() << xi18nc("@info:status", "Copying a new partition: Creating a new partition instead.");
140
141 Partition* newPartition = new Partition(newOp->newPartition());
142 newPartition->setFirstSector(pushedCopyOp->copiedPartition().firstSector());
143 newPartition->fileSystem().setFirstSector(pushedCopyOp->copiedPartition().fileSystem().firstSector());
144 newPartition->setLastSector(pushedCopyOp->copiedPartition().lastSector());
145 newPartition->fileSystem().setLastSector(pushedCopyOp->copiedPartition().fileSystem().lastSector());
146
147 NewOperation* revisedNewOp = new NewOperation(pushedCopyOp->targetDevice(), newPartition);
148 delete pushedOp;
149 pushedOp = revisedNewOp;
150
151 return true;
152 }
153
154 // -- 4 --
155 if (pushedLabelOp && &newOp->newPartition() == &pushedLabelOp->labeledPartition()) {
156 Log() << xi18nc("@info:status", "Changing label for a new partition: No new operation required.");
157
158 newOp->setLabelJob()->setLabel(pushedLabelOp->newLabel());
159 newOp->newPartition().fileSystem().setLabel(pushedLabelOp->newLabel());
160
161 delete pushedOp;
162 pushedOp = nullptr;
163
164 return true;
165 }
166
167 // -- 5 --
168 if (pushedCreateFileSystemOp && &newOp->newPartition() == &pushedCreateFileSystemOp->partition()) {
169 Log() << xi18nc("@info:status", "Changing file system for a new partition: No new operation required.");
170
171 FileSystem* oldFs = &newOp->newPartition().fileSystem();
172
173 newOp->newPartition().setFileSystem(FileSystemFactory::cloneWithNewType(pushedCreateFileSystemOp->newFileSystem()->type(), *oldFs));
174
175 delete oldFs;
176 oldFs = nullptr;
177
178 delete pushedOp;
179 pushedOp = nullptr;
180
181 return true;
182 }
183
184 // -- 6 --
185 if (pushedCheckOp && &newOp->newPartition() == &pushedCheckOp->checkedPartition()) {
186 Log() << xi18nc("@info:status", "Checking file systems is automatically done when creating them: No new operation required.");
187
188 delete pushedOp;
189 pushedOp = nullptr;
190
191 return true;
192 }
193
194 return false;
195 }
196
197 /** Tries to merge an existing CopyOperation with a new Operation pushed on the OperationStack.
198
199 These are the cases to consider:
200
201 <ol>
202 <!-- 1 -->
203 <li>An existing CopyOperation created a Partition that is now being deleted. Remove the
204 CopyOperation, and, if the CopyOperation was an overwrite, carry on with the delete. Else
205 also remove the DeleteOperation.</li>
206
207 <!-- 2 -->
208 <li>An existing CopyOperation created a Partition that is now being copied. We're not copying
209 the target of this existing CopyOperation, but its source instead. If this merge is not done,
210 copied partitions will have misleading labels ("copy of sdXY" instead of "copy of copy of
211 sdXY" for a second-generation copy) but the Operation itself will still work.</li>
212 </ol>
213
214 @param currentOp the Operation already on the stack to try to merge with
215 @param pushedOp the newly pushed Operation
216 @return true if the OperationStack has been modified in a way that requires merging to stop
217 */
mergeCopyOperation(Operation * & currentOp,Operation * & pushedOp)218 bool OperationStack::mergeCopyOperation(Operation*& currentOp, Operation*& pushedOp)
219 {
220 CopyOperation* copyOp = dynamic_cast<CopyOperation*>(currentOp);
221
222 if (copyOp == nullptr)
223 return false;
224
225 DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
226 CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
227
228 // -- 1 --
229 if (pushedDeleteOp && ©Op->copiedPartition() == &pushedDeleteOp->deletedPartition()) {
230 // If the copy operation didn't overwrite, but create a new partition, just remove the
231 // copy operation, forget the delete and be done.
232 if (copyOp->overwrittenPartition() == nullptr) {
233 Log() << xi18nc("@info:status", "Deleting a partition just copied: Removing the copy.");
234
235 delete pushedOp;
236 pushedOp = nullptr;
237 } else {
238 Log() << xi18nc("@info:status", "Deleting a partition just copied over an existing partition: Removing the copy and deleting the existing partition.");
239
240 pushedDeleteOp->setDeletedPartition(copyOp->overwrittenPartition());
241 }
242
243 copyOp->undo();
244 delete operations().takeAt(operations().indexOf(copyOp));
245
246 return true;
247 }
248
249 // -- 2 --
250 if (pushedCopyOp && ©Op->copiedPartition() == &pushedCopyOp->sourcePartition()) {
251 Log() << xi18nc("@info:status", "Copying a partition that is itself a copy: Copying the original source partition instead.");
252
253 pushedCopyOp->setSourcePartition(©Op->sourcePartition());
254 }
255
256 return false;
257 }
258
259 /** Tries to merge an existing RestoreOperation with a new Operation pushed on the OperationStack.
260
261 If an existing RestoreOperation created a Partition that is now being deleted, remove the
262 RestoreOperation, and, if the RestoreOperation was an overwrite, carry on with the delete. Else
263 also remove the DeleteOperation.
264
265 @param currentOp the Operation already on the stack to try to merge with
266 @param pushedOp the newly pushed Operation
267 @return true if the OperationStack has been modified in a way that requires merging to stop
268 */
mergeRestoreOperation(Operation * & currentOp,Operation * & pushedOp)269 bool OperationStack::mergeRestoreOperation(Operation*& currentOp, Operation*& pushedOp)
270 {
271 RestoreOperation* restoreOp = dynamic_cast<RestoreOperation*>(currentOp);
272
273 if (restoreOp == nullptr)
274 return false;
275
276 DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
277
278 if (pushedDeleteOp && &restoreOp->restorePartition() == &pushedDeleteOp->deletedPartition()) {
279 if (restoreOp->overwrittenPartition() == nullptr) {
280 Log() << xi18nc("@info:status", "Deleting a partition just restored: Removing the restore operation.");
281
282 delete pushedOp;
283 pushedOp = nullptr;
284 } else {
285 Log() << xi18nc("@info:status", "Deleting a partition just restored to an existing partition: Removing the restore operation and deleting the existing partition.");
286
287 pushedDeleteOp->setDeletedPartition(restoreOp->overwrittenPartition());
288 }
289
290 restoreOp->undo();
291 delete operations().takeAt(operations().indexOf(restoreOp));
292
293 return true;
294 }
295
296 return false;
297 }
298
299 /** Tries to merge an existing SetPartFlagsOperation with a new Operation pushed on the OperationStack.
300
301 If the Partition flags for an existing Partition are modified look if there is an existing
302 Operation for the same Partition and modify that one.
303
304 @param currentOp the Operation already on the stack to try to merge with
305 @param pushedOp the newly pushed Operation
306 @return true if the OperationStack has been modified in a way that requires merging to stop
307 */
mergePartFlagsOperation(Operation * & currentOp,Operation * & pushedOp)308 bool OperationStack::mergePartFlagsOperation(Operation*& currentOp, Operation*& pushedOp)
309 {
310 SetPartFlagsOperation* partFlagsOp = dynamic_cast<SetPartFlagsOperation*>(currentOp);
311
312 if (partFlagsOp == nullptr)
313 return false;
314
315 SetPartFlagsOperation* pushedFlagsOp = dynamic_cast<SetPartFlagsOperation*>(pushedOp);
316
317 if (pushedFlagsOp && &partFlagsOp->flagPartition() == &pushedFlagsOp->flagPartition()) {
318 Log() << xi18nc("@info:status", "Changing flags again for the same partition: Removing old operation.");
319
320 pushedFlagsOp->setOldFlags(partFlagsOp->oldFlags());
321 partFlagsOp->undo();
322 delete operations().takeAt(operations().indexOf(partFlagsOp));
323
324 return true;
325 }
326
327 return false;
328 }
329
330 /** Tries to merge an existing SetFileSystemLabelOperation with a new Operation pushed on the OperationStack.
331
332 If a FileSystem label for an existing Partition is modified look if there is an existing
333 SetFileSystemLabelOperation for the same Partition.
334
335 @param currentOp the Operation already on the stack to try to merge with
336 @param pushedOp the newly pushed Operation
337 @return true if the OperationStack has been modified in a way that requires merging to stop
338 */
mergePartLabelOperation(Operation * & currentOp,Operation * & pushedOp)339 bool OperationStack::mergePartLabelOperation(Operation*& currentOp, Operation*& pushedOp)
340 {
341 SetFileSystemLabelOperation* partLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(currentOp);
342
343 if (partLabelOp == nullptr)
344 return false;
345
346 SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
347
348 if (pushedLabelOp && &partLabelOp->labeledPartition() == &pushedLabelOp->labeledPartition()) {
349 Log() << xi18nc("@info:status", "Changing label again for the same partition: Removing old operation.");
350
351 pushedLabelOp->setOldLabel(partLabelOp->oldLabel());
352 partLabelOp->undo();
353 delete operations().takeAt(operations().indexOf(partLabelOp));
354
355 return true;
356 }
357
358 return false;
359 }
360
361 /** Tries to merge an existing CreatePartitionTableOperation with a new Operation pushed on the OperationStack.
362
363 If a new partition table is to be created on a device and a previous operation targets that
364 device, remove this previous operation.
365
366 @param currentOp the Operation already on the stack to try to merge with
367 @param pushedOp the newly pushed Operation
368 @return true if the OperationStack has been modified in a way that requires merging to stop
369 */
mergeCreatePartitionTableOperation(Operation * & currentOp,Operation * & pushedOp)370 bool OperationStack::mergeCreatePartitionTableOperation(Operation*& currentOp, Operation*& pushedOp)
371 {
372 CreatePartitionTableOperation* pushedCreatePartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(pushedOp);
373
374 if (pushedCreatePartitionTableOp && currentOp->targets(pushedCreatePartitionTableOp->targetDevice())) {
375 Log() << xi18nc("@info:status", "Creating new partition table, discarding previous operation on device.");
376
377 CreatePartitionTableOperation* createPartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(currentOp);
378 if (createPartitionTableOp != nullptr)
379 pushedCreatePartitionTableOp->setOldPartitionTable(createPartitionTableOp->oldPartitionTable());
380
381 currentOp->undo();
382 delete operations().takeAt(operations().indexOf(currentOp));
383
384 return true;
385 }
386
387 return false;
388 }
389
mergeResizeVolumeGroupResizeOperation(Operation * & pushedOp)390 bool OperationStack::mergeResizeVolumeGroupResizeOperation(Operation*& pushedOp)
391 {
392 ResizeVolumeGroupOperation* pushedResizeVolumeGroupOp = dynamic_cast<ResizeVolumeGroupOperation*>(pushedOp);
393
394 if (pushedResizeVolumeGroupOp && pushedResizeVolumeGroupOp->jobs().count() == 0) {
395 Log() << xi18nc("@info:status", "Resizing Volume Group, nothing to do.");
396
397 return true;
398 }
399
400 return false;
401 }
402
403 /** Pushes a new Operation on the OperationStack.
404
405 This method will call all methods that try to merge the new Operation with the
406 existing ones. It is not uncommon that any of these will delete the pushed
407 Operation. Callers <b>must not rely</b> on the pushed Operation to exist after
408 calling OperationStack::push().
409
410 @param o Pointer to the Operation. Must not be nullptr.
411 */
push(Operation * o)412 void OperationStack::push(Operation* o)
413 {
414 Q_ASSERT(o);
415
416 if (mergeResizeVolumeGroupResizeOperation(o))
417 return;
418
419 for (auto currentOp = operations().rbegin(); currentOp != operations().rend(); ++currentOp) {
420 if (mergeNewOperation(*currentOp, o))
421 break;
422
423 if (mergeCopyOperation(*currentOp, o))
424 break;
425
426 if (mergeRestoreOperation(*currentOp, o))
427 break;
428
429 if (mergePartFlagsOperation(*currentOp, o))
430 break;
431
432 if (mergePartLabelOperation(*currentOp, o))
433 break;
434
435 if (mergeCreatePartitionTableOperation(*currentOp, o))
436 break;
437 }
438
439 if (o != nullptr) {
440 Log() << xi18nc("@info:status", "Add operation: %1", o->description());
441 operations().append(o);
442 o->preview();
443 o->setStatus(Operation::StatusPending);
444 }
445
446 // Q_EMIT operationsChanged even if o is nullptr because it has been merged: merging might
447 // have led to an existing operation changing.
448 Q_EMIT operationsChanged();
449 }
450
451 /** Removes the topmost Operation from the OperationStack, calls Operation::undo() on it and deletes it. */
pop()452 void OperationStack::pop()
453 {
454 Operation* o = operations().takeLast();
455 o->undo();
456 delete o;
457 Q_EMIT operationsChanged();
458 }
459
460 /** Check whether previous operations involve given partition.
461
462 @param p Pointer to the Partition. Must not be nullptr.
463 */
contains(const Partition * p) const464 bool OperationStack::contains(const Partition* p) const
465 {
466 Q_ASSERT(p);
467
468 for (const auto &o : operations()) {
469 if (o->targets(*p))
470 return true;
471
472 CopyOperation* copyOp = dynamic_cast<CopyOperation*>(o);
473 if (copyOp) {
474 const Partition* source = ©Op->sourcePartition();
475 if (source == p)
476 return true;
477 }
478 }
479
480 return false;
481 }
482
483 /** Removes all Operations from the OperationStack, calling Operation::undo() on them and deleting them. */
clearOperations()484 void OperationStack::clearOperations()
485 {
486 while (!operations().isEmpty()) {
487 Operation* o = operations().takeLast();
488 if (o->status() == Operation::StatusPending)
489 o->undo();
490 delete o;
491 }
492
493 Q_EMIT operationsChanged();
494 }
495
496 /** Clears the list of Devices. */
clearDevices()497 void OperationStack::clearDevices()
498 {
499 QWriteLocker lockDevices(&lock());
500
501 qDeleteAll(previewDevices());
502 previewDevices().clear();
503 Q_EMIT devicesChanged();
504 }
505
506 /** Finds a Device a Partition is on.
507 @param p pointer to the Partition to find a Device for
508 @return the Device or nullptr if none could be found
509 */
findDeviceForPartition(const Partition * p)510 Device* OperationStack::findDeviceForPartition(const Partition* p)
511 {
512 QReadLocker lockDevices(&lock());
513
514 const auto devices = previewDevices();
515 for (Device *d : devices) {
516 if (d->partitionTable() == nullptr)
517 continue;
518
519 const auto partitions = d->partitionTable()->children();
520 for (const auto *part : partitions) {
521 if (part == p)
522 return d;
523
524 for (const auto &child : part->children())
525 if (child == p)
526 return d;
527 }
528 }
529
530 return nullptr;
531 }
532
533 /** Adds a Device to the OperationStack
534 @param d pointer to the Device to add. Must not be nullptr.
535 */
addDevice(Device * d)536 void OperationStack::addDevice(Device* d)
537 {
538 Q_ASSERT(d);
539
540 QWriteLocker lockDevices(&lock());
541
542 previewDevices().append(d);
543 Q_EMIT devicesChanged();
544 }
545
deviceLessThan(const Device * d1,const Device * d2)546 static bool deviceLessThan(const Device* d1, const Device* d2)
547 {
548 // Display alphabetically sorted disk devices above LVM VGs
549 if (d1->type() == Device::Type::LVM_Device && d2->type() == Device::Type::Disk_Device )
550 return false;
551
552 return d1->deviceNode() <= d2->deviceNode();
553 }
554
sortDevices()555 void OperationStack::sortDevices()
556 {
557 QWriteLocker lockDevices(&lock());
558
559 std::sort(previewDevices().begin(), previewDevices().end(), deviceLessThan);
560
561 Q_EMIT devicesChanged();
562 }
563