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