xref: /haiku/src/apps/haikudepot/ui/App.cpp (revision 474e0be84223769a9df416a80083fc3eae78f961)
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 
346 	if (message->FindString(KEY_ALERT_TEXT, &alertText) != B_OK)
347 		alertText = "?";
348 
349 	if (message->FindString(KEY_ALERT_TITLE, &alertTitle) != B_OK)
350 		alertTitle = B_TRANSLATE("Error");
351 
352 	BAlert* alert = new BAlert(alertTitle, alertText, B_TRANSLATE("OK"));
353 
354 	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
355 	alert->Go();
356 }
357 
358 
359 // #pragma mark - private
360 
361 
362 void
363 App::_Open(const BEntry& entry)
364 {
365 	BPath path;
366 	if (!entry.Exists() || entry.GetPath(&path) != B_OK) {
367 		fprintf(stderr, "Package file not found: %s\n", path.Path());
368 		return;
369 	}
370 
371 	// Try to parse package file via Package Kit
372 	BPackageKit::BPackageInfo info;
373 	status_t status = info.ReadFromPackageFile(path.Path());
374 	if (status != B_OK) {
375 		fprintf(stderr, "Failed to parse package file: %s\n",
376 			strerror(status));
377 		return;
378 	}
379 
380 	// Transfer information into PackageInfo
381 	PackageInfoRef package(new(std::nothrow) PackageInfo(info), true);
382 	if (!package.IsSet()) {
383 		fprintf(stderr, "Could not allocate PackageInfo\n");
384 		return;
385 	}
386 
387 	package->SetLocalFilePath(path.Path());
388 
389 	// Set if the package is active
390 	//
391 	// TODO(leavengood): It is very awkward having to check these two locations
392 	// here, and in many other places in HaikuDepot. Why do clients of the
393 	// package kit have to know about these locations?
394 	bool active = false;
395 	BPackageKit::BPackageRoster roster;
396 	status = roster.IsPackageActive(
397 		BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, info, &active);
398 	if (status != B_OK) {
399 		fprintf(stderr, "Could not check if package was active in system: %s\n",
400 			strerror(status));
401 		return;
402 	}
403 	if (!active) {
404 		status = roster.IsPackageActive(
405 			BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME, info, &active);
406 		if (status != B_OK) {
407 			fprintf(stderr,
408 				"Could not check if package was active in home: %s\n",
409 				strerror(status));
410 			return;
411 		}
412 	}
413 
414 	if (active) {
415 		package->SetState(ACTIVATED);
416 	}
417 
418 	BMessage settings;
419 	_LoadSettings(settings);
420 
421 	MainWindow* window = new MainWindow(settings, package);
422 	_ShowWindow(window);
423 }
424 
425 
426 void
427 App::_ShowWindow(MainWindow* window)
428 {
429 	window->Show();
430 	fWindowCount++;
431 }
432 
433 
434 bool
435 App::_LoadSettings(BMessage& settings)
436 {
437 	if (!fSettingsRead) {
438 		fSettingsRead = true;
439 		if (load_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot") != B_OK)
440 			fSettings.MakeEmpty();
441 	}
442 	settings = fSettings;
443 	return !fSettings.IsEmpty();
444 }
445 
446 
447 void
448 App::_StoreSettings(const BMessage& settings)
449 {
450 	// Take what is in settings and replace data under the same name in
451 	// fSettings, leaving anything in fSettings that is not contained in
452 	// settings.
453 	int32 i = 0;
454 
455 	char* name;
456 	type_code type;
457 	int32 count;
458 
459 	while (settings.GetInfo(B_ANY_TYPE, i++, &name, &type, &count) == B_OK) {
460 		fSettings.RemoveName(name);
461 		for (int32 j = 0; j < count; j++) {
462 			const void* data;
463 			ssize_t size;
464 			if (settings.FindData(name, type, j, &data, &size) != B_OK)
465 				break;
466 			fSettings.AddData(name, type, data, size);
467 		}
468 	}
469 
470 	save_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot");
471 }
472 
473 
474 // #pragma mark -
475 
476 
477 static const char* kPackageDaemonSignature
478 	= "application/x-vnd.haiku-package_daemon";
479 
480 void
481 App::_CheckPackageDaemonRuns()
482 {
483 	while (!be_roster->IsRunning(kPackageDaemonSignature)) {
484 		BAlert* alert = new BAlert(
485 			B_TRANSLATE("Start package daemon"),
486 			B_TRANSLATE("HaikuDepot needs the package daemon to function, "
487 				"and it appears to be not running.\n"
488 				"Would you like to start it now?"),
489 			B_TRANSLATE("No, quit HaikuDepot"),
490 			B_TRANSLATE("Start package daemon"), NULL, B_WIDTH_AS_USUAL,
491 			B_WARNING_ALERT);
492 		alert->SetShortcut(0, B_ESCAPE);
493 
494 		if (alert->Go() == 0)
495 			HDFATAL("unable to start without the package daemon running");
496 
497 		if (!_LaunchPackageDaemon())
498 			break;
499 	}
500 }
501 
502 
503 bool
504 App::_LaunchPackageDaemon()
505 {
506 	status_t ret = be_roster->Launch(kPackageDaemonSignature);
507 	if (ret != B_OK) {
508 		BString errorMessage
509 			= B_TRANSLATE("Starting the package daemon failed:\n\n%Error%");
510 		errorMessage.ReplaceAll("%Error%", strerror(ret));
511 
512 		BAlert* alert = new BAlert(
513 			B_TRANSLATE("Package daemon problem"), errorMessage,
514 			B_TRANSLATE("Quit HaikuDepot"),
515 			B_TRANSLATE("Try again"), NULL, B_WIDTH_AS_USUAL,
516 			B_WARNING_ALERT);
517 		alert->SetShortcut(0, B_ESCAPE);
518 
519 		if (alert->Go() == 0)
520 			return false;
521 	}
522 	// TODO: Would be nice to send a message to the package daemon instead
523 	// and get a reply once it is ready.
524 	snooze(2000000);
525 	return true;
526 }
527 
528 
529 /*static*/ bool
530 App::_CheckIsFirstRun()
531 {
532 	BPath testFilePath;
533 	bool exists = false;
534 	status_t status = StorageUtils::LocalWorkingFilesPath("testfile.txt",
535 		testFilePath, false);
536 	if (status != B_OK) {
537 		HDERROR("unable to establish the location of the test file");
538 	}
539 	else
540 		status = StorageUtils::ExistsObject(testFilePath, &exists, NULL, NULL);
541 	return !exists;
542 }
543 
544 
545 /*! \brief Checks to ensure that a working file is able to be written.
546     \return false if the startup should be stopped and the application should
547             quit.
548 */
549 
550 bool
551 App::_CheckTestFile()
552 {
553 	BPath testFilePath;
554 	BString pathDescription = "???";
555 	status_t result = StorageUtils::LocalWorkingFilesPath("testfile.txt",
556 		testFilePath, false);
557 
558 	if (result == B_OK) {
559 		pathDescription = testFilePath.Path();
560 		result = StorageUtils::CheckCanWriteTo(testFilePath);
561 	}
562 
563 	if (result != B_OK) {
564 		StorageUtils::SetWorkingFilesUnavailable();
565 
566 		BString msg = B_TRANSLATE("This application writes and reads some"
567 			" working files on your computer in order to function. It appears"
568 			" that there are problems writing a test file at [%TestFilePath%]."
569 			" Check that there are no issues with your local disk or"
570 			" permissions that might prevent this application from writing"
571 			" files into that directory location. You may choose to acknowledge"
572 			" this problem and continue, but some functionality may be"
573 			" disabled.");
574 		msg.ReplaceAll("%TestFilePath%", pathDescription);
575 
576 		BAlert* alert = new(std::nothrow) BAlert(
577 			B_TRANSLATE("Problem with working files"),
578 			msg,
579 			B_TRANSLATE("Quit"), B_TRANSLATE("Continue"));
580 
581 		if (alert->Go() == 0)
582 			return false;
583 	}
584 
585 	return true;
586 }
587 
588 
589 /*!	This method will check to see if the version of the application has changed.
590 	If it has changed then it will delete all of the contents of the cache
591 	directory.  This will mean that when application logic changes, it need not
592 	bother to migrate the cached files.  Also any old cached files will be
593 	cleared out that no longer serve any purpose.
594 
595 	Errors arising in this logic need not prevent the application from failing
596 	to start as this is just a clean-up.
597 */
598 
599 void
600 App::_ClearCacheOnVersionChange()
601 {
602 	BString version;
603 
604 	if (AppUtils::GetAppVersionString(version) != B_OK) {
605 		HDERROR("clear cache; unable to get the application version");
606 		return;
607 	}
608 
609 	BPath lastVersionPath;
610 	if (StorageUtils::LocalWorkingFilesPath(
611 			"version.txt", lastVersionPath) != B_OK) {
612 		HDERROR("clear cache; unable to get version file path");
613 		return;
614 	}
615 
616 	bool exists;
617 	off_t size;
618 
619 	if (StorageUtils::ExistsObject(
620 		lastVersionPath, &exists, NULL, &size) != B_OK) {
621 		HDERROR("clear cache; unable to check version file exists");
622 		return;
623 	}
624 
625 	BString lastVersion;
626 
627 	if (exists && StorageUtils::AppendToString(lastVersionPath, lastVersion)
628 			!= B_OK) {
629 		HDERROR("clear cache; unable to read the version from [%s]",
630 			lastVersionPath.Path());
631 		return;
632 	}
633 
634 	if (lastVersion != version) {
635 		HDINFO("last version [%s] and current version [%s] do not match"
636 			" -> will flush cache", lastVersion.String(), version.String());
637 		StorageUtils::RemoveWorkingDirectoryContents();
638 		HDINFO("will write version [%s] to [%s]",
639 			version.String(), lastVersionPath.Path());
640 		StorageUtils::AppendToFile(version, lastVersionPath);
641 	} else {
642 		HDINFO("last version [%s] and current version [%s] match"
643 		 	" -> cache retained", lastVersion.String(), version.String());
644 	}
645 }
646