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