xref: /haiku/src/apps/haikudepot/ui/App.cpp (revision 4a32f48e70297d9a634646f01e08c2f451ecd6bd)
1 /*
2  * Copyright 2013, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2017-2021, Andrew Lindesay <apl@lindesay.co.nz>.
4  * All rights reserved. Distributed under the terms of the MIT License.
5  */
6 
7 
8 #include "App.h"
9 
10 #include <stdio.h>
11 
12 #include <Alert.h>
13 #include <Catalog.h>
14 #include <Entry.h>
15 #include <Message.h>
16 #include <package/PackageDefs.h>
17 #include <package/PackageInfo.h>
18 #include <package/PackageRoster.h>
19 #include <Path.h>
20 #include <Roster.h>
21 #include <Screen.h>
22 #include <String.h>
23 
24 #include "support.h"
25 
26 #include "AppUtils.h"
27 #include "FeaturedPackagesView.h"
28 #include "Logger.h"
29 #include "MainWindow.h"
30 #include "PackageIconTarRepository.h"
31 #include "ServerHelper.h"
32 #include "ServerSettings.h"
33 #include "ScreenshotWindow.h"
34 #include "StorageUtils.h"
35 
36 
37 #undef B_TRANSLATION_CONTEXT
38 #define B_TRANSLATION_CONTEXT "App"
39 
40 
41 App::App()
42 	:
43 	BApplication("application/x-vnd.Haiku-HaikuDepot"),
44 	fMainWindow(NULL),
45 	fWindowCount(0),
46 	fSettingsRead(false)
47 {
48 	srand((unsigned int) time(NULL));
49 	_CheckPackageDaemonRuns();
50 	fIsFirstRun = _CheckIsFirstRun();
51 }
52 
53 
54 App::~App()
55 {
56 	// We cannot let global destructors cleanup static BitmapRef objects,
57 	// since calling BBitmap destructors needs a valid BApplication still
58 	// around. That's why we do it here.
59 	PackageIconTarRepository::CleanupDefaultIcon();
60 	FeaturedPackagesView::CleanupIcons();
61 	ScreenshotWindow::CleanupIcons();
62 }
63 
64 
65 bool
66 App::QuitRequested()
67 {
68 	if (fMainWindow != NULL
69 		&& fMainWindow->LockLooperWithTimeout(1500000) == B_OK) {
70 		BMessage windowSettings;
71 		fMainWindow->StoreSettings(windowSettings);
72 
73 		fMainWindow->UnlockLooper();
74 
75 		_StoreSettings(windowSettings);
76 	}
77 
78 	return BApplication::QuitRequested();
79 }
80 
81 
82 void
83 App::ReadyToRun()
84 {
85 	if (fWindowCount > 0)
86 		return;
87 
88 	BMessage settings;
89 	_LoadSettings(settings);
90 
91 	if (!_CheckTestFile()) {
92 		Quit();
93 		return;
94 	}
95 
96 	_ClearCacheOnVersionChange();
97 
98 	fMainWindow = new MainWindow(settings);
99 	_ShowWindow(fMainWindow);
100 }
101 
102 
103 bool
104 App::IsFirstRun()
105 {
106 	return fIsFirstRun;
107 }
108 
109 
110 void
111 App::MessageReceived(BMessage* message)
112 {
113 	switch (message->what) {
114 		case MSG_MAIN_WINDOW_CLOSED:
115 		{
116 			BMessage windowSettings;
117 			if (message->FindMessage(KEY_WINDOW_SETTINGS,
118 					&windowSettings) == B_OK) {
119 				_StoreSettings(windowSettings);
120 			}
121 
122 			fWindowCount--;
123 			if (fWindowCount == 0)
124 				Quit();
125 			break;
126 		}
127 
128 		case MSG_CLIENT_TOO_OLD:
129 			ServerHelper::AlertClientTooOld(message);
130 			break;
131 
132 		case MSG_NETWORK_TRANSPORT_ERROR:
133 			ServerHelper::AlertTransportError(message);
134 			break;
135 
136 		case MSG_SERVER_ERROR:
137 			ServerHelper::AlertServerJsonRpcError(message);
138 			break;
139 
140 		case MSG_ALERT_SIMPLE_ERROR:
141 			_AlertSimpleError(message);
142 			break;
143 
144 		case MSG_SERVER_DATA_CHANGED:
145 			fMainWindow->PostMessage(message);
146 			break;
147 
148 		default:
149 			BApplication::MessageReceived(message);
150 			break;
151 	}
152 }
153 
154 
155 void
156 App::RefsReceived(BMessage* message)
157 {
158 	entry_ref ref;
159 	int32 index = 0;
160 	while (message->FindRef("refs", index++, &ref) == B_OK) {
161 		BEntry entry(&ref, true);
162 		_Open(entry);
163 	}
164 }
165 
166 
167 enum arg_switch {
168 	UNKNOWN_SWITCH,
169 	NOT_SWITCH,
170 	HELP_SWITCH,
171 	WEB_APP_BASE_URL_SWITCH,
172 	VERBOSITY_SWITCH,
173 	FORCE_NO_NETWORKING_SWITCH,
174 	PREFER_CACHE_SWITCH,
175 	DROP_CACHE_SWITCH
176 };
177 
178 
179 static void
180 app_print_help()
181 {
182 	fprintf(stdout, "HaikuDepot ");
183 	fprintf(stdout, "[-u|--webappbaseurl <web-app-base-url>]\n");
184 	fprintf(stdout, "[-v|--verbosity [off|info|debug|trace]\n");
185 	fprintf(stdout, "[--nonetworking]\n");
186 	fprintf(stdout, "[--prefercache]\n");
187 	fprintf(stdout, "[--dropcache]\n");
188 	fprintf(stdout, "[-h|--help]\n");
189 	fprintf(stdout, "\n");
190 	fprintf(stdout, "'-h' : causes this help text to be printed out.\n");
191 	fprintf(stdout, "'-v' : allows for the verbosity level to be set.\n");
192 	fprintf(stdout, "'-u' : allows for the haiku depot server url to be\n");
193 	fprintf(stdout, "   configured.\n");
194 	fprintf(stdout, "'--nonetworking' : prevents network access.\n");
195 	fprintf(stdout, "'--prefercache' : prefer to get data from cache rather\n");
196 	fprintf(stdout, "  then obtain data from the network.**\n");
197 	fprintf(stdout, "'--dropcache' : drop cached data before performing\n");
198 	fprintf(stdout, "  bulk operations.**\n");
199 	fprintf(stdout, "\n");
200 	fprintf(stdout, "** = only applies to bulk operations.\n");
201 }
202 
203 
204 static arg_switch
205 app_resolve_switch(char *arg)
206 {
207 	int arglen = strlen(arg);
208 
209 	if (arglen > 0 && arg[0] == '-') {
210 
211 		if (arglen > 3 && arg[1] == '-') { // long form
212 			if (0 == strcmp(&arg[2], "webappbaseurl"))
213 				return WEB_APP_BASE_URL_SWITCH;
214 
215 			if (0 == strcmp(&arg[2], "help"))
216 				return HELP_SWITCH;
217 
218 			if (0 == strcmp(&arg[2], "verbosity"))
219 				return VERBOSITY_SWITCH;
220 
221 			if (0 == strcmp(&arg[2], "nonetworking"))
222 				return FORCE_NO_NETWORKING_SWITCH;
223 
224 			if (0 == strcmp(&arg[2], "prefercache"))
225 				return PREFER_CACHE_SWITCH;
226 
227 			if (0 == strcmp(&arg[2], "dropcache"))
228 				return DROP_CACHE_SWITCH;
229 		} else {
230 			if (arglen == 2) { // short form
231 				switch (arg[1]) {
232 					case 'u':
233 						return WEB_APP_BASE_URL_SWITCH;
234 
235 					case 'h':
236 						return HELP_SWITCH;
237 
238 					case 'v':
239 						return VERBOSITY_SWITCH;
240 				}
241 			}
242 		}
243 
244 		return UNKNOWN_SWITCH;
245 	}
246 
247 	return NOT_SWITCH;
248 }
249 
250 
251 void
252 App::ArgvReceived(int32 argc, char* argv[])
253 {
254 	for (int i = 1; i < argc;) {
255 
256 			// check to make sure that if there is a value for the switch,
257 			// that the value is in fact supplied.
258 
259 		switch (app_resolve_switch(argv[i])) {
260 			case VERBOSITY_SWITCH:
261 			case WEB_APP_BASE_URL_SWITCH:
262 				if (i == argc-1) {
263 					fprintf(stdout, "unexpected end of arguments; missing "
264 						"value for switch [%s]\n", argv[i]);
265 					Quit();
266 					return;
267 				}
268 				break;
269 
270 			default:
271 				break;
272 		}
273 
274 			// now process each switch.
275 
276 		switch (app_resolve_switch(argv[i])) {
277 
278 			case VERBOSITY_SWITCH:
279 				if (!Logger::SetLevelByName(argv[i+1])) {
280 					fprintf(stdout, "unknown log level [%s]\n", argv[i + 1]);
281 					Quit();
282 				}
283 				i++; // also move past the log level value
284 				break;
285 
286 			case HELP_SWITCH:
287 				app_print_help();
288 				Quit();
289 				break;
290 
291 			case WEB_APP_BASE_URL_SWITCH:
292 				if (ServerSettings::SetBaseUrl(BUrl(argv[i + 1])) != B_OK) {
293 					fprintf(stdout, "malformed web app base url; %s\n",
294 						argv[i + 1]);
295 					Quit();
296 				}
297 				else {
298 					fprintf(stdout, "did configure the web base url; %s\n",
299 						argv[i + 1]);
300 				}
301 
302 				i++; // also move past the url value
303 
304 				break;
305 
306 			case FORCE_NO_NETWORKING_SWITCH:
307 				ServerSettings::SetForceNoNetwork(true);
308 				break;
309 
310 			case PREFER_CACHE_SWITCH:
311 				ServerSettings::SetPreferCache(true);
312 				break;
313 
314 			case DROP_CACHE_SWITCH:
315 				ServerSettings::SetDropCache(true);
316 				break;
317 
318 			case NOT_SWITCH:
319 			{
320 				BEntry entry(argv[i], true);
321 				_Open(entry);
322 				break;
323 			}
324 
325 			case UNKNOWN_SWITCH:
326 				fprintf(stdout, "unknown switch; %s\n", argv[i]);
327 				Quit();
328 				break;
329 		}
330 
331 		i++; // move on at least one arg
332 	}
333 }
334 
335 
336 /*! This method will display an alert based on a message.  This message arrives
337     from a number of possible background threads / processes in the application.
338 */
339 
340 void
341 App::_AlertSimpleError(BMessage* message)
342 {
343 	BString alertTitle;
344 	BString alertText;
345 	int32 typeInt;
346 
347 	if (message->FindString(KEY_ALERT_TEXT, &alertText) != B_OK)
348 		alertText = "?";
349 
350 	if (message->FindString(KEY_ALERT_TITLE, &alertTitle) != B_OK)
351 		alertTitle = B_TRANSLATE("Error");
352 
353 	if (message->FindInt32(KEY_ALERT_TYPE, &typeInt) != B_OK)
354 		typeInt = B_INFO_ALERT;
355 
356 	BAlert* alert = new BAlert(alertTitle, alertText, B_TRANSLATE("OK"),
357 		NULL, NULL, B_WIDTH_AS_USUAL, static_cast<alert_type>(typeInt));
358 
359 	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
360 	alert->Go();
361 }
362 
363 
364 // #pragma mark - private
365 
366 
367 void
368 App::_Open(const BEntry& entry)
369 {
370 	BPath path;
371 	if (!entry.Exists() || entry.GetPath(&path) != B_OK) {
372 		fprintf(stderr, "Package file not found: %s\n", path.Path());
373 		return;
374 	}
375 
376 	// Try to parse package file via Package Kit
377 	BPackageKit::BPackageInfo info;
378 	status_t status = info.ReadFromPackageFile(path.Path());
379 	if (status != B_OK) {
380 		fprintf(stderr, "Failed to parse package file: %s\n",
381 			strerror(status));
382 		return;
383 	}
384 
385 	// Transfer information into PackageInfo
386 	PackageInfoRef package(new(std::nothrow) PackageInfo(info), true);
387 	if (!package.IsSet()) {
388 		fprintf(stderr, "Could not allocate PackageInfo\n");
389 		return;
390 	}
391 
392 	package->SetLocalFilePath(path.Path());
393 
394 	// Set if the package is active
395 	//
396 	// TODO(leavengood): It is very awkward having to check these two locations
397 	// here, and in many other places in HaikuDepot. Why do clients of the
398 	// package kit have to know about these locations?
399 	bool active = false;
400 	BPackageKit::BPackageRoster roster;
401 	status = roster.IsPackageActive(
402 		BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, info, &active);
403 	if (status != B_OK) {
404 		fprintf(stderr, "Could not check if package was active in system: %s\n",
405 			strerror(status));
406 		return;
407 	}
408 	if (!active) {
409 		status = roster.IsPackageActive(
410 			BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME, info, &active);
411 		if (status != B_OK) {
412 			fprintf(stderr,
413 				"Could not check if package was active in home: %s\n",
414 				strerror(status));
415 			return;
416 		}
417 	}
418 
419 	if (active) {
420 		package->SetState(ACTIVATED);
421 	}
422 
423 	BMessage settings;
424 	_LoadSettings(settings);
425 
426 	MainWindow* window = new MainWindow(settings, package);
427 	_ShowWindow(window);
428 }
429 
430 
431 void
432 App::_ShowWindow(MainWindow* window)
433 {
434 	window->Show();
435 	fWindowCount++;
436 }
437 
438 
439 bool
440 App::_LoadSettings(BMessage& settings)
441 {
442 	if (!fSettingsRead) {
443 		fSettingsRead = true;
444 		if (load_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot") != B_OK)
445 			fSettings.MakeEmpty();
446 	}
447 	settings = fSettings;
448 	return !fSettings.IsEmpty();
449 }
450 
451 
452 void
453 App::_StoreSettings(const BMessage& settings)
454 {
455 	// Take what is in settings and replace data under the same name in
456 	// fSettings, leaving anything in fSettings that is not contained in
457 	// settings.
458 	int32 i = 0;
459 
460 	char* name;
461 	type_code type;
462 	int32 count;
463 
464 	while (settings.GetInfo(B_ANY_TYPE, i++, &name, &type, &count) == B_OK) {
465 		fSettings.RemoveName(name);
466 		for (int32 j = 0; j < count; j++) {
467 			const void* data;
468 			ssize_t size;
469 			if (settings.FindData(name, type, j, &data, &size) != B_OK)
470 				break;
471 			fSettings.AddData(name, type, data, size);
472 		}
473 	}
474 
475 	save_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot");
476 }
477 
478 
479 // #pragma mark -
480 
481 
482 static const char* kPackageDaemonSignature
483 	= "application/x-vnd.haiku-package_daemon";
484 
485 void
486 App::_CheckPackageDaemonRuns()
487 {
488 	while (!be_roster->IsRunning(kPackageDaemonSignature)) {
489 		BAlert* alert = new BAlert(
490 			B_TRANSLATE("Start package daemon"),
491 			B_TRANSLATE("HaikuDepot needs the package daemon to function, "
492 				"and it appears to be not running.\n"
493 				"Would you like to start it now?"),
494 			B_TRANSLATE("No, quit HaikuDepot"),
495 			B_TRANSLATE("Start package daemon"), NULL, B_WIDTH_AS_USUAL,
496 			B_WARNING_ALERT);
497 		alert->SetShortcut(0, B_ESCAPE);
498 
499 		if (alert->Go() == 0)
500 			HDFATAL("unable to start without the package daemon running");
501 
502 		if (!_LaunchPackageDaemon())
503 			break;
504 	}
505 }
506 
507 
508 bool
509 App::_LaunchPackageDaemon()
510 {
511 	status_t ret = be_roster->Launch(kPackageDaemonSignature);
512 	if (ret != B_OK) {
513 		BString errorMessage
514 			= B_TRANSLATE("Starting the package daemon failed:\n\n%Error%");
515 		errorMessage.ReplaceAll("%Error%", strerror(ret));
516 
517 		BAlert* alert = new BAlert(
518 			B_TRANSLATE("Package daemon problem"), errorMessage,
519 			B_TRANSLATE("Quit HaikuDepot"),
520 			B_TRANSLATE("Try again"), NULL, B_WIDTH_AS_USUAL,
521 			B_WARNING_ALERT);
522 		alert->SetShortcut(0, B_ESCAPE);
523 
524 		if (alert->Go() == 0)
525 			return false;
526 	}
527 	// TODO: Would be nice to send a message to the package daemon instead
528 	// and get a reply once it is ready.
529 	snooze(2000000);
530 	return true;
531 }
532 
533 
534 /*static*/ bool
535 App::_CheckIsFirstRun()
536 {
537 	BPath testFilePath;
538 	bool exists = false;
539 	status_t status = StorageUtils::LocalWorkingFilesPath("testfile.txt",
540 		testFilePath, false);
541 	if (status != B_OK) {
542 		HDERROR("unable to establish the location of the test file");
543 	}
544 	else
545 		status = StorageUtils::ExistsObject(testFilePath, &exists, NULL, NULL);
546 	return !exists;
547 }
548 
549 
550 /*! \brief Checks to ensure that a working file is able to be written.
551     \return false if the startup should be stopped and the application should
552             quit.
553 */
554 
555 bool
556 App::_CheckTestFile()
557 {
558 	BPath testFilePath;
559 	BString pathDescription = "???";
560 	status_t result = StorageUtils::LocalWorkingFilesPath("testfile.txt",
561 		testFilePath, false);
562 
563 	if (result == B_OK) {
564 		pathDescription = testFilePath.Path();
565 		result = StorageUtils::CheckCanWriteTo(testFilePath);
566 	}
567 
568 	if (result != B_OK) {
569 		StorageUtils::SetWorkingFilesUnavailable();
570 
571 		BString msg = B_TRANSLATE("This application writes and reads some"
572 			" working files on your computer in order to function. It appears"
573 			" that there are problems writing a test file at [%TestFilePath%]."
574 			" Check that there are no issues with your local disk or"
575 			" permissions that might prevent this application from writing"
576 			" files into that directory location. You may choose to acknowledge"
577 			" this problem and continue, but some functionality may be"
578 			" disabled.");
579 		msg.ReplaceAll("%TestFilePath%", pathDescription);
580 
581 		BAlert* alert = new(std::nothrow) BAlert(
582 			B_TRANSLATE("Problem with working files"),
583 			msg,
584 			B_TRANSLATE("Quit"), B_TRANSLATE("Continue"));
585 
586 		if (alert->Go() == 0)
587 			return false;
588 	}
589 
590 	return true;
591 }
592 
593 
594 /*!	This method will check to see if the version of the application has changed.
595 	If it has changed then it will delete all of the contents of the cache
596 	directory.  This will mean that when application logic changes, it need not
597 	bother to migrate the cached files.  Also any old cached files will be
598 	cleared out that no longer serve any purpose.
599 
600 	Errors arising in this logic need not prevent the application from failing
601 	to start as this is just a clean-up.
602 */
603 
604 void
605 App::_ClearCacheOnVersionChange()
606 {
607 	BString version;
608 
609 	if (AppUtils::GetAppVersionString(version) != B_OK) {
610 		HDERROR("clear cache; unable to get the application version");
611 		return;
612 	}
613 
614 	BPath lastVersionPath;
615 	if (StorageUtils::LocalWorkingFilesPath(
616 			"version.txt", lastVersionPath) != B_OK) {
617 		HDERROR("clear cache; unable to get version file path");
618 		return;
619 	}
620 
621 	bool exists;
622 	off_t size;
623 
624 	if (StorageUtils::ExistsObject(
625 		lastVersionPath, &exists, NULL, &size) != B_OK) {
626 		HDERROR("clear cache; unable to check version file exists");
627 		return;
628 	}
629 
630 	BString lastVersion;
631 
632 	if (exists && StorageUtils::AppendToString(lastVersionPath, lastVersion)
633 			!= B_OK) {
634 		HDERROR("clear cache; unable to read the version from [%s]",
635 			lastVersionPath.Path());
636 		return;
637 	}
638 
639 	if (lastVersion != version) {
640 		HDINFO("last version [%s] and current version [%s] do not match"
641 			" -> will flush cache", lastVersion.String(), version.String());
642 		StorageUtils::RemoveWorkingDirectoryContents();
643 		HDINFO("will write version [%s] to [%s]",
644 			version.String(), lastVersionPath.Path());
645 		StorageUtils::AppendToFile(version, lastVersionPath);
646 	} else {
647 		HDINFO("last version [%s] and current version [%s] match"
648 		 	" -> cache retained", lastVersion.String(), version.String());
649 	}
650 }
651