xref: /haiku/src/apps/installer/WorkerThread.cpp (revision 89d652d5e0defd9d095c778709cef82f5f10c357)
1 /*
2  * Copyright 2009, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2005-2008, Jérôme DUVAL.
4  * All rights reserved. Distributed under the terms of the MIT License.
5  */
6 
7 #include "WorkerThread.h"
8 
9 #include <stdio.h>
10 
11 #include <Alert.h>
12 #include <Autolock.h>
13 #include <Catalog.h>
14 #include <Directory.h>
15 #include <DiskDeviceVisitor.h>
16 #include <DiskDeviceTypes.h>
17 #include <FindDirectory.h>
18 #include <Locale.h>
19 #include <Menu.h>
20 #include <MenuItem.h>
21 #include <Message.h>
22 #include <Messenger.h>
23 #include <Path.h>
24 #include <String.h>
25 #include <VolumeRoster.h>
26 
27 #include "AutoLocker.h"
28 #include "CopyEngine.h"
29 #include "InstallerWindow.h"
30 #include "PackageViews.h"
31 #include "PartitionMenuItem.h"
32 #include "ProgressReporter.h"
33 #include "StringForSize.h"
34 #include "UnzipEngine.h"
35 
36 
37 #define B_TRANSLATE_CONTEXT "InstallProgress"
38 
39 
40 //#define COPY_TRACE
41 #ifdef COPY_TRACE
42 #define CALLED() 		printf("CALLED %s\n",__PRETTY_FUNCTION__)
43 #define ERR2(x, y...)	fprintf(stderr, "WorkerThread: "x" %s\n", y, strerror(err))
44 #define ERR(x)			fprintf(stderr, "WorkerThread: "x" %s\n", strerror(err))
45 #else
46 #define CALLED()
47 #define ERR(x)
48 #define ERR2(x, y...)
49 #endif
50 
51 const char BOOT_PATH[] = "/boot";
52 
53 const uint32 MSG_START_INSTALLING = 'eSRT';
54 
55 
56 class SourceVisitor : public BDiskDeviceVisitor {
57 public:
58 	SourceVisitor(BMenu* menu);
59 	virtual bool Visit(BDiskDevice* device);
60 	virtual bool Visit(BPartition* partition, int32 level);
61 
62 private:
63 	BMenu* fMenu;
64 };
65 
66 
67 class TargetVisitor : public BDiskDeviceVisitor {
68 public:
69 	TargetVisitor(BMenu* menu);
70 	virtual bool Visit(BDiskDevice* device);
71 	virtual bool Visit(BPartition* partition, int32 level);
72 
73 private:
74 	BMenu* fMenu;
75 };
76 
77 
78 // #pragma mark - WorkerThread
79 
80 
81 WorkerThread::WorkerThread(InstallerWindow *window)
82 	:
83 	BLooper("copy_engine"),
84 	fWindow(window),
85 	fPackages(NULL),
86 	fSpaceRequired(0),
87 	fCancelSemaphore(-1)
88 {
89 	Run();
90 }
91 
92 
93 void
94 WorkerThread::MessageReceived(BMessage* message)
95 {
96 	CALLED();
97 
98 	switch (message->what) {
99 		case MSG_START_INSTALLING:
100 			_PerformInstall(fWindow->GetSourceMenu(), fWindow->GetTargetMenu());
101 			break;
102 
103 		case MSG_WRITE_BOOT_SECTOR:
104 		{
105 			int32 id;
106 			if (message->FindInt32("id", &id) != B_OK) {
107 				_SetStatusMessage(B_TRANSLATE("Boot sector not written "
108 					"because of an internal error."));
109 				break;
110 			}
111 
112 			// TODO: Refactor with _PerformInstall()
113 			BPath targetDirectory;
114 			BDiskDevice device;
115 			BPartition* partition;
116 
117 			if (fDDRoster.GetPartitionWithID(id, &device, &partition) == B_OK) {
118 				if (!partition->IsMounted()) {
119 					if (partition->Mount() < B_OK) {
120 						_SetStatusMessage(B_TRANSLATE("The partition can't be "
121 							"mounted. Please choose a different partition."));
122 						break;
123 					}
124 				}
125 				if (partition->GetMountPoint(&targetDirectory) != B_OK) {
126 					_SetStatusMessage(B_TRANSLATE("The mount point could not "
127 						"be retrieved."));
128 					break;
129 				}
130 			} else if (fDDRoster.GetDeviceWithID(id, &device) == B_OK) {
131 				if (!device.IsMounted()) {
132 					if (device.Mount() < B_OK) {
133 						_SetStatusMessage(B_TRANSLATE("The disk can't be "
134 							"mounted. Please choose a different disk."));
135 						break;
136 					}
137 				}
138 				if (device.GetMountPoint(&targetDirectory) != B_OK) {
139 					_SetStatusMessage(B_TRANSLATE("The mount point could not "
140 						"be retrieved."));
141 					break;
142 				}
143 			}
144 
145 			_LaunchFinishScript(targetDirectory);
146 			// TODO: Get error from executing script!
147 			_SetStatusMessage(
148 				B_TRANSLATE("Boot sector successfully written."));
149 		}
150 		default:
151 			BLooper::MessageReceived(message);
152 	}
153 }
154 
155 
156 
157 
158 void
159 WorkerThread::ScanDisksPartitions(BMenu *srcMenu, BMenu *targetMenu)
160 {
161 	// NOTE: This is actually executed in the window thread.
162 	BDiskDevice device;
163 	BPartition *partition = NULL;
164 
165 	printf("\nScanDisksPartitions source partitions begin\n");
166 	SourceVisitor srcVisitor(srcMenu);
167 	fDDRoster.VisitEachMountedPartition(&srcVisitor, &device, &partition);
168 
169 	printf("\nScanDisksPartitions target partitions begin\n");
170 	TargetVisitor targetVisitor(targetMenu);
171 	fDDRoster.VisitEachPartition(&targetVisitor, &device, &partition);
172 }
173 
174 
175 void
176 WorkerThread::SetPackagesList(BList *list)
177 {
178 	// Executed in window thread.
179 	BAutolock _(this);
180 
181 	delete fPackages;
182 	fPackages = list;
183 }
184 
185 
186 void
187 WorkerThread::StartInstall()
188 {
189 	// Executed in window thread.
190 	PostMessage(MSG_START_INSTALLING, this);
191 }
192 
193 
194 void
195 WorkerThread::WriteBootSector(BMenu* targetMenu)
196 {
197 	// Executed in window thread.
198 	CALLED();
199 
200 	PartitionMenuItem* item = (PartitionMenuItem*)targetMenu->FindMarked();
201 	if (item == NULL) {
202 		ERR("bad menu items\n");
203 		return;
204 	}
205 
206 	BMessage message(MSG_WRITE_BOOT_SECTOR);
207 	message.AddInt32("id", item->ID());
208 	PostMessage(&message, this);
209 }
210 
211 
212 // #pragma mark -
213 
214 
215 void
216 WorkerThread::_LaunchInitScript(BPath &path)
217 {
218 	BPath bootPath;
219 	find_directory(B_BEOS_BOOT_DIRECTORY, &bootPath);
220 	BString command("/bin/sh ");
221 	command += bootPath.Path();
222 	command += "/InstallerInitScript ";
223 	command += "\"";
224 	command += path.Path();
225 	command += "\"";
226 	_SetStatusMessage(B_TRANSLATE("Starting Installation."));
227 	system(command.String());
228 }
229 
230 
231 void
232 WorkerThread::_LaunchFinishScript(BPath &path)
233 {
234 	BPath bootPath;
235 	find_directory(B_BEOS_BOOT_DIRECTORY, &bootPath);
236 	BString command("/bin/sh ");
237 	command += bootPath.Path();
238 	command += "/InstallerFinishScript ";
239 	command += "\"";
240 	command += path.Path();
241 	command += "\"";
242 	_SetStatusMessage(B_TRANSLATE("Finishing Installation."));
243 	system(command.String());
244 }
245 
246 
247 void
248 WorkerThread::_PerformInstall(BMenu* srcMenu, BMenu* targetMenu)
249 {
250 	CALLED();
251 
252 	BPath targetDirectory, srcDirectory, trashPath, testPath;
253 	BDirectory targetDir;
254 	BDiskDevice device;
255 	BPartition* partition;
256 	BVolume targetVolume;
257 	status_t err = B_OK;
258 	int32 entries = 0;
259 	entry_ref testRef;
260 	const char* mountError = B_TRANSLATE("The disk can't be mounted. Please "
261 		"choose a different disk.");
262 
263 	BMessenger messenger(fWindow);
264 	ProgressReporter reporter(messenger, new BMessage(MSG_STATUS_MESSAGE));
265 	CopyEngine engine(&reporter);
266 	BList unzipEngines;
267 
268 	PartitionMenuItem* targetItem = (PartitionMenuItem*)targetMenu->FindMarked();
269 	PartitionMenuItem* srcItem = (PartitionMenuItem*)srcMenu->FindMarked();
270 	if (!srcItem || !targetItem) {
271 		ERR("bad menu items\n");
272 		goto error;
273 	}
274 
275 	// check if target is initialized
276 	// ask if init or mount as is
277 	if (fDDRoster.GetPartitionWithID(targetItem->ID(), &device,
278 			&partition) == B_OK) {
279 		if (!partition->IsMounted()) {
280 			if ((err = partition->Mount()) < B_OK) {
281 				_SetStatusMessage(mountError);
282 				ERR("BPartition::Mount");
283 				goto error;
284 			}
285 		}
286 		if ((err = partition->GetVolume(&targetVolume)) != B_OK) {
287 			ERR("BPartition::GetVolume");
288 			goto error;
289 		}
290 		if ((err = partition->GetMountPoint(&targetDirectory)) != B_OK) {
291 			ERR("BPartition::GetMountPoint");
292 			goto error;
293 		}
294 	} else if (fDDRoster.GetDeviceWithID(targetItem->ID(), &device) == B_OK) {
295 		if (!device.IsMounted()) {
296 			if ((err = device.Mount()) < B_OK) {
297 				_SetStatusMessage(mountError);
298 				ERR("BDiskDevice::Mount");
299 				goto error;
300 			}
301 		}
302 		if ((err = device.GetVolume(&targetVolume)) != B_OK) {
303 			ERR("BDiskDevice::GetVolume");
304 			goto error;
305 		}
306 		if ((err = device.GetMountPoint(&targetDirectory)) != B_OK) {
307 			ERR("BDiskDevice::GetMountPoint");
308 			goto error;
309 		}
310 	} else
311 		goto error; // shouldn't happen
312 
313 	// check if target has enough space
314 	if ((fSpaceRequired > 0 && targetVolume.FreeBytes() < fSpaceRequired)
315 		&& ((new BAlert("", B_TRANSLATE("The destination disk may not have "
316 			"enough space. Try choosing a different disk or choose to not "
317 			"install optional items."), B_TRANSLATE("Try installing anyway"),
318 			B_TRANSLATE("Cancel"), 0,
319 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
320 		goto error;
321 	}
322 
323 	if (fDDRoster.GetPartitionWithID(srcItem->ID(), &device, &partition) == B_OK) {
324 		if ((err = partition->GetMountPoint(&srcDirectory)) != B_OK) {
325 			ERR("BPartition::GetMountPoint");
326 			goto error;
327 		}
328 	} else if (fDDRoster.GetDeviceWithID(srcItem->ID(), &device) == B_OK) {
329 		if ((err = device.GetMountPoint(&srcDirectory)) != B_OK) {
330 			ERR("BDiskDevice::GetMountPoint");
331 			goto error;
332 		}
333 	} else
334 		goto error; // shouldn't happen
335 
336 	// check not installing on itself
337 	if (strcmp(srcDirectory.Path(), targetDirectory.Path()) == 0) {
338 		_SetStatusMessage(B_TRANSLATE("You can't install the contents of a "
339 			"disk onto itself. Please choose a different disk."));
340 		goto error;
341 	}
342 
343 	// check not installing on boot volume
344 	if ((strncmp(BOOT_PATH, targetDirectory.Path(), strlen(BOOT_PATH)) == 0)
345 		&& ((new BAlert("", B_TRANSLATE("Are you sure you want to install "
346 			"onto the current boot disk? The Installer will have to reboot "
347 			"your machine if you proceed."), B_TRANSLATE("OK"),
348 			B_TRANSLATE("Cancel"), 0,
349 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
350 		_SetStatusMessage("Installation stopped.");
351 		goto error;
352 	}
353 
354 	// check if target volume's trash dir has anything in it
355 	// (target volume w/ only an empty trash dir is considered
356 	// an empty volume)
357 	if (find_directory(B_TRASH_DIRECTORY, &trashPath, false,
358 		&targetVolume) == B_OK && targetDir.SetTo(trashPath.Path()) == B_OK) {
359 			while (targetDir.GetNextRef(&testRef) == B_OK) {
360 				// Something in the Trash
361 				entries++;
362 				break;
363 			}
364 	}
365 
366 	targetDir.SetTo(targetDirectory.Path());
367 
368 	// check if target volume otherwise has any entries
369 	while (entries == 0 && targetDir.GetNextRef(&testRef) == B_OK) {
370 		if (testPath.SetTo(&testRef) == B_OK && testPath != trashPath)
371 			entries++;
372 	}
373 
374 	if (entries != 0
375 		&& ((new BAlert("", B_TRANSLATE("The target volume is not empty. Are "
376 			"you sure you want to install anyway?\n\nNote: The 'system' folder "
377 			"will be a clean copy from the source volume, all other folders "
378 			"will be merged, whereas files and links that exist on both the "
379 			"source and target volume will be overwritten with the source "
380 			"volume version."),
381 			B_TRANSLATE("Install anyway"), B_TRANSLATE("Cancel"), 0,
382 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
383 		// TODO: Would be cool to offer the option here to clean additional
384 		// folders at the user's choice (like /boot/common and /boot/develop).
385 		err = B_CANCELED;
386 		goto error;
387 	}
388 
389 	// Begin actual installation
390 
391 	_LaunchInitScript(targetDirectory);
392 
393 	// let the engine collect information for the progress bar later on
394 	engine.ResetTargets(srcDirectory.Path());
395 	err = engine.CollectTargets(srcDirectory.Path(), fCancelSemaphore);
396 	if (err != B_OK)
397 		goto error;
398 
399 	// collect selected packages also
400 	if (fPackages) {
401 		BPath pkgRootDir(srcDirectory.Path(), PACKAGES_DIRECTORY);
402 		int32 count = fPackages->CountItems();
403 		for (int32 i = 0; i < count; i++) {
404 			Package *p = static_cast<Package*>(fPackages->ItemAt(i));
405 			BPath packageDir(pkgRootDir.Path(), p->Folder());
406 			err = engine.CollectTargets(packageDir.Path(), fCancelSemaphore);
407 			if (err != B_OK)
408 				goto error;
409 		}
410 	}
411 
412 	// collect information about all zip packages
413 	err = _ProcessZipPackages(srcDirectory.Path(), targetDirectory.Path(),
414 		&reporter, unzipEngines);
415 	if (err != B_OK)
416 		goto error;
417 
418 	reporter.StartTimer();
419 
420 	// copy source volume
421 	err = engine.CopyFolder(srcDirectory.Path(), targetDirectory.Path(),
422 		fCancelSemaphore);
423 	if (err != B_OK)
424 		goto error;
425 
426 	// copy selected packages
427 	if (fPackages) {
428 		BPath pkgRootDir(srcDirectory.Path(), PACKAGES_DIRECTORY);
429 		int32 count = fPackages->CountItems();
430 		for (int32 i = 0; i < count; i++) {
431 			Package *p = static_cast<Package*>(fPackages->ItemAt(i));
432 			BPath packageDir(pkgRootDir.Path(), p->Folder());
433 			err = engine.CopyFolder(packageDir.Path(), targetDirectory.Path(),
434 				fCancelSemaphore);
435 			if (err != B_OK)
436 				goto error;
437 		}
438 	}
439 
440 	// Extract all zip packages. If an error occured, delete the rest of
441 	// the engines, but stop extracting.
442 	for (int32 i = 0; i < unzipEngines.CountItems(); i++) {
443 		UnzipEngine* engine = reinterpret_cast<UnzipEngine*>(
444 			unzipEngines.ItemAtFast(i));
445 		if (err == B_OK)
446 			err = engine->UnzipPackage();
447 		delete engine;
448 	}
449 	if (err != B_OK)
450 		goto error;
451 
452 	_LaunchFinishScript(targetDirectory);
453 
454 	BMessenger(fWindow).SendMessage(MSG_INSTALL_FINISHED);
455 
456 	return;
457 error:
458 	BMessage statusMessage(MSG_RESET);
459 	if (err == B_CANCELED)
460 		_SetStatusMessage(B_TRANSLATE("Installation canceled."));
461 	else
462 		statusMessage.AddInt32("error", err);
463 	ERR("_PerformInstall failed");
464 	BMessenger(fWindow).SendMessage(&statusMessage);
465 }
466 
467 
468 status_t
469 WorkerThread::_ProcessZipPackages(const char* sourcePath,
470 	const char* targetPath, ProgressReporter* reporter, BList& unzipEngines)
471 {
472 	// TODO: Put those in the optional packages list view
473 	// TODO: Implement mechanism to handle dependencies between these
474 	// packages. (Selecting one will auto-select others.)
475 	BPath pkgRootDir(sourcePath, PACKAGES_DIRECTORY);
476 	BDirectory directory(pkgRootDir.Path());
477 	BEntry entry;
478 	while (directory.GetNextEntry(&entry) == B_OK) {
479 		char name[B_FILE_NAME_LENGTH];
480 		if (entry.GetName(name) != B_OK)
481 			continue;
482 		int nameLength = strlen(name);
483 		if (nameLength <= 0)
484 			continue;
485 		char* nameExtension = name + nameLength - 4;
486 		if (strcasecmp(nameExtension, ".zip") != 0)
487 			continue;
488 		printf("found .zip package: %s\n", name);
489 
490 		UnzipEngine* unzipEngine = new(std::nothrow) UnzipEngine(reporter,
491 			fCancelSemaphore);
492 		if (unzipEngine == NULL || !unzipEngines.AddItem(unzipEngine)) {
493 			delete unzipEngine;
494 			return B_NO_MEMORY;
495 		}
496 		BPath path;
497 		entry.GetPath(&path);
498 		status_t ret = unzipEngine->SetTo(path.Path(), targetPath);
499 		if (ret != B_OK)
500 			return ret;
501 
502 		reporter->AddItems(unzipEngine->ItemsToUncompress(),
503 			unzipEngine->BytesToUncompress());
504 	}
505 
506 	return B_OK;
507 }
508 
509 
510 void
511 WorkerThread::_SetStatusMessage(const char *status)
512 {
513 	BMessage msg(MSG_STATUS_MESSAGE);
514 	msg.AddString("status", status);
515 	BMessenger(fWindow).SendMessage(&msg);
516 }
517 
518 
519 static void
520 make_partition_label(BPartition* partition, char* label, char* menuLabel,
521 	bool showContentType)
522 {
523 	char size[20];
524 	string_for_size(partition->Size(), size, sizeof(size));
525 
526 	BPath path;
527 	partition->GetPath(&path);
528 
529 	if (showContentType) {
530 		const char* type = partition->ContentType();
531 		if (type == NULL)
532 			type = B_TRANSLATE_COMMENT("Unknown Type", "Partition content type");
533 
534 		sprintf(label, "%s - %s [%s] (%s)", partition->ContentName(), size,
535 			path.Path(), type);
536 	} else {
537 		sprintf(label, "%s - %s [%s]", partition->ContentName(), size,
538 			path.Path());
539 	}
540 
541 	sprintf(menuLabel, "%s - %s", partition->ContentName(), size);
542 }
543 
544 
545 // #pragma mark - SourceVisitor
546 
547 
548 SourceVisitor::SourceVisitor(BMenu *menu)
549 	: fMenu(menu)
550 {
551 }
552 
553 bool
554 SourceVisitor::Visit(BDiskDevice *device)
555 {
556 	return Visit(device, 0);
557 }
558 
559 
560 bool
561 SourceVisitor::Visit(BPartition *partition, int32 level)
562 {
563 	BPath path;
564 	if (partition->GetPath(&path) == B_OK)
565 		printf("SourceVisitor::Visit(BPartition *) : %s\n", path.Path());
566 	printf("SourceVisitor::Visit(BPartition *) : %s\n",
567 		partition->ContentName());
568 
569 	if (partition->ContentType() == NULL)
570 		return false;
571 
572 	bool isBootPartition = false;
573 	if (partition->IsMounted()) {
574 		BPath mountPoint;
575 		partition->GetMountPoint(&mountPoint);
576 		isBootPartition = strcmp(BOOT_PATH, mountPoint.Path()) == 0;
577 	}
578 
579 	if (!isBootPartition
580 		&& strcmp(partition->ContentType(), kPartitionTypeBFS) != 0) {
581 		// Except only BFS partitions, except this is the boot partition
582 		// (ISO9660 with write overlay for example).
583 		return false;
584 	}
585 
586 	// TODO: We could probably check if this volume contains
587 	// the Haiku kernel or something. Does it make sense to "install"
588 	// from your BFS volume containing the music collection?
589 	// TODO: Then the check for BFS could also be removed above.
590 
591 	char label[255];
592 	char menuLabel[255];
593 	make_partition_label(partition, label, menuLabel, false);
594 	PartitionMenuItem* item = new PartitionMenuItem(partition->ContentName(),
595 		label, menuLabel, new BMessage(SOURCE_PARTITION), partition->ID());
596 	item->SetMarked(isBootPartition);
597 	fMenu->AddItem(item);
598 	return false;
599 }
600 
601 
602 // #pragma mark - TargetVisitor
603 
604 
605 TargetVisitor::TargetVisitor(BMenu *menu)
606 	: fMenu(menu)
607 {
608 }
609 
610 
611 bool
612 TargetVisitor::Visit(BDiskDevice *device)
613 {
614 	if (device->IsReadOnlyMedia())
615 		return false;
616 	return Visit(device, 0);
617 }
618 
619 
620 bool
621 TargetVisitor::Visit(BPartition *partition, int32 level)
622 {
623 	BPath path;
624 	if (partition->GetPath(&path) == B_OK)
625 		printf("TargetVisitor::Visit(BPartition *) : %s\n", path.Path());
626 	printf("TargetVisitor::Visit(BPartition *) : %s\n",
627 		partition->ContentName());
628 
629 	if (partition->ContentSize() < 20 * 1024 * 1024) {
630 		// reject partitions which are too small anyway
631 		// TODO: Could depend on the source size
632 		printf("  too small\n");
633 		return false;
634 	}
635 
636 	if (partition->CountChildren() > 0) {
637 		// Looks like an extended partition, or the device itself.
638 		// Do not accept this as target...
639 		printf("  no leaf partition\n");
640 		return false;
641 	}
642 
643 	// TODO: After running DriveSetup and doing another scan, it would
644 	// be great to pick the partition which just appeared!
645 
646 	// Only BFS partitions are valid targets, but we want to display the
647 	// other partitions as well, in order not to irritate the user.
648 	bool isValidTarget = partition->ContentType() != NULL
649 		&& strcmp(partition->ContentType(), kPartitionTypeBFS) == 0;
650 
651 	char label[255];
652 	char menuLabel[255];
653 	make_partition_label(partition, label, menuLabel, !isValidTarget);
654 	PartitionMenuItem* item = new PartitionMenuItem(partition->ContentName(),
655 		label, menuLabel, new BMessage(TARGET_PARTITION), partition->ID());
656 
657 	item->SetIsValidTarget(isValidTarget);
658 
659 
660 	fMenu->AddItem(item);
661 	return false;
662 }
663 
664