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