xref: /haiku/src/apps/installer/WorkerThread.cpp (revision c0cd8cf1999b2266ed949f079916c1d35cd387fa)
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 <Menu.h>
19 #include <MenuItem.h>
20 #include <Message.h>
21 #include <Messenger.h>
22 #include <Path.h>
23 #include <String.h>
24 #include <VolumeRoster.h>
25 
26 #include "AutoLocker.h"
27 #include "CopyEngine.h"
28 #include "InstallerWindow.h"
29 #include "PackageViews.h"
30 #include "PartitionMenuItem.h"
31 #include "ProgressReporter.h"
32 #include "UnzipEngine.h"
33 
34 
35 #define TR_CONTEXT "InstallProgress"
36 
37 
38 //#define COPY_TRACE
39 #ifdef COPY_TRACE
40 #define CALLED() 		printf("CALLED %s\n",__PRETTY_FUNCTION__)
41 #define ERR2(x, y...)	fprintf(stderr, "WorkerThread: "x" %s\n", y, strerror(err))
42 #define ERR(x)			fprintf(stderr, "WorkerThread: "x" %s\n", strerror(err))
43 #else
44 #define CALLED()
45 #define ERR(x)
46 #define ERR2(x, y...)
47 #endif
48 
49 const char BOOT_PATH[] = "/boot";
50 
51 extern void SizeAsString(off_t size, char* string);
52 
53 
54 const uint32 MSG_START_INSTALLING = 'eSRT';
55 
56 
57 class SourceVisitor : public BDiskDeviceVisitor {
58 public:
59 	SourceVisitor(BMenu* menu);
60 	virtual bool Visit(BDiskDevice* device);
61 	virtual bool Visit(BPartition* partition, int32 level);
62 
63 private:
64 	BMenu* fMenu;
65 };
66 
67 
68 class TargetVisitor : public BDiskDeviceVisitor {
69 public:
70 	TargetVisitor(BMenu* menu);
71 	virtual bool Visit(BDiskDevice* device);
72 	virtual bool Visit(BPartition* partition, int32 level);
73 
74 private:
75 	BMenu* fMenu;
76 };
77 
78 
79 // #pragma mark - WorkerThread
80 
81 
82 WorkerThread::WorkerThread(InstallerWindow *window)
83 	:
84 	BLooper("copy_engine"),
85 	fWindow(window),
86 	fPackages(NULL),
87 	fSpaceRequired(0),
88 	fCancelSemaphore(-1)
89 {
90 	Run();
91 }
92 
93 
94 void
95 WorkerThread::MessageReceived(BMessage* message)
96 {
97 	CALLED();
98 
99 	switch (message->what) {
100 		case MSG_START_INSTALLING:
101 			_PerformInstall(fWindow->GetSourceMenu(), fWindow->GetTargetMenu());
102 			break;
103 
104 		case MSG_WRITE_BOOT_SECTOR:
105 		{
106 			int32 id;
107 			if (message->FindInt32("id", &id) != B_OK) {
108 				_SetStatusMessage(TR("Boot sector not written because of an "
109 					" internal error."));
110 				break;
111 			}
112 
113 			// TODO: Refactor with _PerformInstall()
114 			BPath targetDirectory;
115 			BDiskDevice device;
116 			BPartition* partition;
117 
118 			if (fDDRoster.GetPartitionWithID(id, &device, &partition) == B_OK) {
119 				if (!partition->IsMounted()) {
120 					if (partition->Mount() < B_OK) {
121 						_SetStatusMessage(TR("The partition can't be mounted. "
122 							"Please choose a different partition."));
123 						break;
124 					}
125 				}
126 				if (partition->GetMountPoint(&targetDirectory) != B_OK) {
127 					_SetStatusMessage(TR("The mount point could not be "
128 						"retrieve."));
129 					break;
130 				}
131 			} else if (fDDRoster.GetDeviceWithID(id, &device) == B_OK) {
132 				if (!device.IsMounted()) {
133 					if (device.Mount() < B_OK) {
134 						_SetStatusMessage(TR("The disk can't be mounted. "
135 							"Please choose a different disk."));
136 						break;
137 					}
138 				}
139 				if (device.GetMountPoint(&targetDirectory) != B_OK) {
140 					_SetStatusMessage(TR("The mount point could not be "
141 						"retrieve."));
142 					break;
143 				}
144 			}
145 
146 			_LaunchFinishScript(targetDirectory);
147 			// TODO: Get error from executing script!
148 			_SetStatusMessage(TR("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(TR("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(TR("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 = TR("The disk can't be mounted. Please choose "
261 		"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("", TR("The destination disk may not have enough "
316 			"space. Try choosing a different disk or choose to not install "
317 			"optional items."), TR("Try installing anyway"), TR("Cancel"), 0,
318 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
319 		goto error;
320 	}
321 
322 	if (fDDRoster.GetPartitionWithID(srcItem->ID(), &device, &partition) == B_OK) {
323 		if ((err = partition->GetMountPoint(&srcDirectory)) != B_OK) {
324 			ERR("BPartition::GetMountPoint");
325 			goto error;
326 		}
327 	} else if (fDDRoster.GetDeviceWithID(srcItem->ID(), &device) == B_OK) {
328 		if ((err = device.GetMountPoint(&srcDirectory)) != B_OK) {
329 			ERR("BDiskDevice::GetMountPoint");
330 			goto error;
331 		}
332 	} else
333 		goto error; // shouldn't happen
334 
335 	// check not installing on itself
336 	if (strcmp(srcDirectory.Path(), targetDirectory.Path()) == 0) {
337 		_SetStatusMessage(TR("You can't install the contents of a disk onto "
338 			"itself. Please choose a different disk."));
339 		goto error;
340 	}
341 
342 	// check not installing on boot volume
343 	if ((strncmp(BOOT_PATH, targetDirectory.Path(), strlen(BOOT_PATH)) == 0)
344 		&& ((new BAlert("", TR("Are you sure you want to install onto the "
345 			"current boot disk? The Installer will have to reboot your "
346 			"machine if you proceed."), TR("OK"), TR("Cancel"), 0,
347 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
348 		_SetStatusMessage("Installation stopped.");
349 		goto error;
350 	}
351 
352 	// check if target volume's trash dir has anything in it
353 	// (target volume w/ only an empty trash dir is considered
354 	// an empty volume)
355 	if (find_directory(B_TRASH_DIRECTORY, &trashPath, false,
356 		&targetVolume) == B_OK && targetDir.SetTo(trashPath.Path()) == B_OK) {
357 			while (targetDir.GetNextRef(&testRef) == B_OK) {
358 				// Something in the Trash
359 				entries++;
360 				break;
361 			}
362 	}
363 
364 	targetDir.SetTo(targetDirectory.Path());
365 
366 	// check if target volume otherwise has any entries
367 	while (entries == 0 && targetDir.GetNextRef(&testRef) == B_OK) {
368 		if (testPath.SetTo(&testRef) == B_OK && testPath != trashPath)
369 			entries++;
370 	}
371 
372 	if (entries != 0
373 		&& ((new BAlert("", TR("The target volume is not empty. Are you sure you "
374 			"want to install anyway?\n\nNote: The 'system' folder will be a "
375 			"clean copy from the source volume, all other folders will be "
376 			"merged, whereas files and links that exist on both the source "
377 			"and target volume will be overwritten with the source volume "
378 			"version."),
379 			TR("Install anyway"), TR("Cancel"), 0,
380 			B_WIDTH_AS_USUAL, B_STOP_ALERT))->Go() != 0)) {
381 		// TODO: Would be cool to offer the option here to clean additional
382 		// folders at the user's choice (like /boot/common and /boot/develop).
383 		err = B_CANCELED;
384 		goto error;
385 	}
386 
387 	// Begin actuall installation
388 
389 	_LaunchInitScript(targetDirectory);
390 
391 	// let the engine collect information for the progress bar later on
392 	engine.ResetTargets();
393 	err = engine.CollectTargets(srcDirectory.Path(), fCancelSemaphore);
394 	if (err != B_OK)
395 		goto error;
396 
397 	// collect selected packages also
398 	if (fPackages) {
399 		BPath pkgRootDir(srcDirectory.Path(), PACKAGES_DIRECTORY);
400 		int32 count = fPackages->CountItems();
401 		for (int32 i = 0; i < count; i++) {
402 			Package *p = static_cast<Package*>(fPackages->ItemAt(i));
403 			BPath packageDir(pkgRootDir.Path(), p->Folder());
404 			err = engine.CollectTargets(packageDir.Path(), fCancelSemaphore);
405 			if (err != B_OK)
406 				goto error;
407 		}
408 	}
409 
410 	// collect information about all zip packages
411 	err = _ProcessZipPackages(srcDirectory.Path(), targetDirectory.Path(),
412 		&reporter, unzipEngines);
413 	if (err != B_OK)
414 		goto error;
415 
416 	reporter.StartTimer();
417 
418 	// copy source volume
419 	err = engine.CopyFolder(srcDirectory.Path(), targetDirectory.Path(),
420 		fCancelSemaphore);
421 	if (err != B_OK)
422 		goto error;
423 
424 	// copy selected packages
425 	if (fPackages) {
426 		BPath pkgRootDir(srcDirectory.Path(), PACKAGES_DIRECTORY);
427 		int32 count = fPackages->CountItems();
428 		for (int32 i = 0; i < count; i++) {
429 			Package *p = static_cast<Package*>(fPackages->ItemAt(i));
430 			BPath packageDir(pkgRootDir.Path(), p->Folder());
431 			err = engine.CopyFolder(packageDir.Path(), targetDirectory.Path(),
432 				fCancelSemaphore);
433 			if (err != B_OK)
434 				goto error;
435 		}
436 	}
437 
438 	// Extract all zip packages. If an error occured, delete the rest of
439 	// the engines, but stop extracting.
440 	for (int32 i = 0; i < unzipEngines.CountItems(); i++) {
441 		UnzipEngine* engine = reinterpret_cast<UnzipEngine*>(
442 			unzipEngines.ItemAtFast(i));
443 		if (err == B_OK)
444 			err = engine->UnzipPackage();
445 		delete engine;
446 	}
447 	if (err != B_OK)
448 		goto error;
449 
450 	_LaunchFinishScript(targetDirectory);
451 
452 	BMessenger(fWindow).SendMessage(MSG_INSTALL_FINISHED);
453 
454 	return;
455 error:
456 	BMessage statusMessage(MSG_RESET);
457 	if (err == B_CANCELED)
458 		_SetStatusMessage(TR("Installation canceled."));
459 	else
460 		statusMessage.AddInt32("error", err);
461 	ERR("_PerformInstall failed");
462 	BMessenger(fWindow).SendMessage(&statusMessage);
463 }
464 
465 
466 status_t
467 WorkerThread::_ProcessZipPackages(const char* sourcePath,
468 	const char* targetPath, ProgressReporter* reporter, BList& unzipEngines)
469 {
470 	// TODO: Put those in the optional packages list view
471 	// TODO: Implement mechanism to handle dependencies between these
472 	// packages. (Selecting one will auto-select others.)
473 	BPath pkgRootDir(sourcePath, PACKAGES_DIRECTORY);
474 	BDirectory directory(pkgRootDir.Path());
475 	BEntry entry;
476 	while (directory.GetNextEntry(&entry) == B_OK) {
477 		char name[B_FILE_NAME_LENGTH];
478 		if (entry.GetName(name) != B_OK)
479 			continue;
480 		int nameLength = strlen(name);
481 		if (nameLength <= 0)
482 			continue;
483 		char* nameExtension = name + nameLength - 4;
484 		if (strcasecmp(nameExtension, ".zip") != 0)
485 			continue;
486 		printf("found .zip package: %s\n", name);
487 
488 		UnzipEngine* unzipEngine = new(std::nothrow) UnzipEngine(reporter,
489 			fCancelSemaphore);
490 		if (unzipEngine == NULL || !unzipEngines.AddItem(unzipEngine)) {
491 			delete unzipEngine;
492 			return B_NO_MEMORY;
493 		}
494 		BPath path;
495 		entry.GetPath(&path);
496 		status_t ret = unzipEngine->SetTo(path.Path(), targetPath);
497 		if (ret != B_OK)
498 			return ret;
499 
500 		reporter->AddItems(unzipEngine->ItemsToUncompress(),
501 			unzipEngine->BytesToUncompress());
502 	}
503 
504 	return B_OK;
505 }
506 
507 
508 void
509 WorkerThread::_SetStatusMessage(const char *status)
510 {
511 	BMessage msg(MSG_STATUS_MESSAGE);
512 	msg.AddString("status", status);
513 	BMessenger(fWindow).SendMessage(&msg);
514 }
515 
516 
517 static void
518 make_partition_label(BPartition* partition, char* label, char* menuLabel,
519 	bool showContentType)
520 {
521 	char size[15];
522 	SizeAsString(partition->Size(), size);
523 
524 	BPath path;
525 	partition->GetPath(&path);
526 
527 	if (showContentType) {
528 		const char* type = partition->ContentType();
529 		if (type == NULL)
530 			type = TR_CMT("Unknown Type", "Partition content type");
531 
532 		sprintf(label, "%s - %s [%s] (%s)", partition->ContentName(), size,
533 			path.Path(), type);
534 	} else {
535 		sprintf(label, "%s - %s [%s]", partition->ContentName(), size,
536 			path.Path());
537 	}
538 
539 	sprintf(menuLabel, "%s - %s", partition->ContentName(), size);
540 }
541 
542 
543 // #pragma mark - SourceVisitor
544 
545 
546 SourceVisitor::SourceVisitor(BMenu *menu)
547 	: fMenu(menu)
548 {
549 }
550 
551 bool
552 SourceVisitor::Visit(BDiskDevice *device)
553 {
554 	return Visit(device, 0);
555 }
556 
557 
558 bool
559 SourceVisitor::Visit(BPartition *partition, int32 level)
560 {
561 	BPath path;
562 	if (partition->GetPath(&path) == B_OK)
563 		printf("SourceVisitor::Visit(BPartition *) : %s\n", path.Path());
564 	printf("SourceVisitor::Visit(BPartition *) : %s\n",
565 		partition->ContentName());
566 
567 	if (partition->ContentType() == NULL)
568 		return false;
569 
570 	bool isBootPartition = false;
571 	if (partition->IsMounted()) {
572 		BPath mountPoint;
573 		partition->GetMountPoint(&mountPoint);
574 		isBootPartition = strcmp(BOOT_PATH, mountPoint.Path()) == 0;
575 	}
576 
577 	if (!isBootPartition
578 		&& strcmp(partition->ContentType(), kPartitionTypeBFS) != 0) {
579 		// Except only BFS partitions, except this is the boot partition
580 		// (ISO9660 with write overlay for example).
581 		return false;
582 	}
583 
584 	// TODO: We could probably check if this volume contains
585 	// the Haiku kernel or something. Does it make sense to "install"
586 	// from your BFS volume containing the music collection?
587 	// TODO: Then the check for BFS could also be removed above.
588 
589 	char label[255];
590 	char menuLabel[255];
591 	make_partition_label(partition, label, menuLabel, false);
592 	PartitionMenuItem* item = new PartitionMenuItem(partition->ContentName(),
593 		label, menuLabel, new BMessage(SOURCE_PARTITION), partition->ID());
594 	item->SetMarked(isBootPartition);
595 	fMenu->AddItem(item);
596 	return false;
597 }
598 
599 
600 // #pragma mark - TargetVisitor
601 
602 
603 TargetVisitor::TargetVisitor(BMenu *menu)
604 	: fMenu(menu)
605 {
606 }
607 
608 
609 bool
610 TargetVisitor::Visit(BDiskDevice *device)
611 {
612 	if (device->IsReadOnlyMedia())
613 		return false;
614 	return Visit(device, 0);
615 }
616 
617 
618 bool
619 TargetVisitor::Visit(BPartition *partition, int32 level)
620 {
621 	BPath path;
622 	if (partition->GetPath(&path) == B_OK)
623 		printf("TargetVisitor::Visit(BPartition *) : %s\n", path.Path());
624 	printf("TargetVisitor::Visit(BPartition *) : %s\n",
625 		partition->ContentName());
626 
627 	if (partition->ContentSize() < 20 * 1024 * 1024) {
628 		// reject partitions which are too small anyway
629 		// TODO: Could depend on the source size
630 		printf("  too small\n");
631 		return false;
632 	}
633 
634 	if (partition->CountChildren() > 0) {
635 		// Looks like an extended partition, or the device itself.
636 		// Do not accept this as target...
637 		printf("  no leaf partition\n");
638 		return false;
639 	}
640 
641 	// TODO: After running DriveSetup and doing another scan, it would
642 	// be great to pick the partition which just appeared!
643 
644 	// Only BFS partitions are valid targets, but we want to display the
645 	// other partitions as well, in order not to irritate the user.
646 	bool isValidTarget = partition->ContentType() != NULL
647 		&& strcmp(partition->ContentType(), kPartitionTypeBFS) == 0;
648 
649 	char label[255];
650 	char menuLabel[255];
651 	make_partition_label(partition, label, menuLabel, !isValidTarget);
652 	PartitionMenuItem* item = new PartitionMenuItem(partition->ContentName(),
653 		label, menuLabel, new BMessage(TARGET_PARTITION), partition->ID());
654 
655 	item->SetIsValidTarget(isValidTarget);
656 
657 
658 	fMenu->AddItem(item);
659 	return false;
660 }
661 
662