xref: /haiku/src/apps/haikudepot/ui/App.cpp (revision 16ad15142c48ee36cd6a807a24efc99c88d4310d)
1 /*
2  * Copyright 2013, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2017-2020, 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 "FeaturedPackagesView.h"
27 #include "Logger.h"
28 #include "MainWindow.h"
29 #include "ServerHelper.h"
30 #include "ServerSettings.h"
31 #include "ScreenshotWindow.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 	PackageInfo::CleanupDefaultIcon();
58 	FeaturedPackagesView::CleanupIcons();
59 	ScreenshotWindow::CleanupIcons();
60 }
61 
62 
63 bool
64 App::QuitRequested()
65 {
66 	if (fMainWindow != NULL
67 		&& fMainWindow->LockLooperWithTimeout(1500000) == B_OK) {
68 		BMessage windowSettings;
69 		fMainWindow->StoreSettings(windowSettings);
70 
71 		fMainWindow->UnlockLooper();
72 
73 		_StoreSettings(windowSettings);
74 	}
75 
76 	return BApplication::QuitRequested();
77 }
78 
79 
80 void
81 App::ReadyToRun()
82 {
83 	if (fWindowCount > 0)
84 		return;
85 
86 	BMessage settings;
87 	_LoadSettings(settings);
88 
89 	if (!_CheckTestFile())
90 	{
91 		Quit();
92 		return;
93 	}
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 
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 	BAlert* alert = new BAlert(alertTitle, alertText, B_TRANSLATE("OK"));
350 
351 	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
352 	alert->Go();
353 }
354 
355 
356 // #pragma mark - private
357 
358 
359 void
360 App::_Open(const BEntry& entry)
361 {
362 	BPath path;
363 	if (!entry.Exists() || entry.GetPath(&path) != B_OK) {
364 		fprintf(stderr, "Package file not found: %s\n", path.Path());
365 		return;
366 	}
367 
368 	// Try to parse package file via Package Kit
369 	BPackageKit::BPackageInfo info;
370 	status_t status = info.ReadFromPackageFile(path.Path());
371 	if (status != B_OK) {
372 		fprintf(stderr, "Failed to parse package file: %s\n",
373 			strerror(status));
374 		return;
375 	}
376 
377 	// Transfer information into PackageInfo
378 	PackageInfoRef package(new(std::nothrow) PackageInfo(info), true);
379 	if (package.Get() == NULL) {
380 		fprintf(stderr, "Could not allocate PackageInfo\n");
381 		return;
382 	}
383 
384 	package->SetLocalFilePath(path.Path());
385 
386 	// Set if the package is active
387 	//
388 	// TODO(leavengood): It is very awkward having to check these two locations
389 	// here, and in many other places in HaikuDepot. Why do clients of the
390 	// package kit have to know about these locations?
391 	bool active = false;
392 	BPackageKit::BPackageRoster roster;
393 	status = roster.IsPackageActive(
394 		BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, info, &active);
395 	if (status != B_OK) {
396 		fprintf(stderr, "Could not check if package was active in system: %s\n",
397 			strerror(status));
398 		return;
399 	}
400 	if (!active) {
401 		status = roster.IsPackageActive(
402 			BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME, info, &active);
403 		if (status != B_OK) {
404 			fprintf(stderr,
405 				"Could not check if package was active in home: %s\n",
406 				strerror(status));
407 			return;
408 		}
409 	}
410 
411 	if (active) {
412 		package->SetState(ACTIVATED);
413 	}
414 
415 	BMessage settings;
416 	_LoadSettings(settings);
417 
418 	MainWindow* window = new MainWindow(settings, package);
419 	_ShowWindow(window);
420 }
421 
422 
423 void
424 App::_ShowWindow(MainWindow* window)
425 {
426 	window->Show();
427 	fWindowCount++;
428 }
429 
430 
431 bool
432 App::_LoadSettings(BMessage& settings)
433 {
434 	if (!fSettingsRead) {
435 		fSettingsRead = true;
436 		if (load_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot") != B_OK)
437 			fSettings.MakeEmpty();
438 	}
439 	settings = fSettings;
440 	return !fSettings.IsEmpty();
441 }
442 
443 
444 void
445 App::_StoreSettings(const BMessage& settings)
446 {
447 	// Take what is in settings and replace data under the same name in
448 	// fSettings, leaving anything in fSettings that is not contained in
449 	// settings.
450 	int32 i = 0;
451 
452 	char* name;
453 	type_code type;
454 	int32 count;
455 
456 	while (settings.GetInfo(B_ANY_TYPE, i++, &name, &type, &count) == B_OK) {
457 		fSettings.RemoveName(name);
458 		for (int32 j = 0; j < count; j++) {
459 			const void* data;
460 			ssize_t size;
461 			if (settings.FindData(name, type, j, &data, &size) != B_OK)
462 				break;
463 			fSettings.AddData(name, type, data, size);
464 		}
465 	}
466 
467 	save_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot");
468 }
469 
470 
471 // #pragma mark -
472 
473 
474 static const char* kPackageDaemonSignature
475 	= "application/x-vnd.haiku-package_daemon";
476 
477 void
478 App::_CheckPackageDaemonRuns()
479 {
480 	while (!be_roster->IsRunning(kPackageDaemonSignature)) {
481 		BAlert* alert = new BAlert(
482 			B_TRANSLATE("Start package daemon"),
483 			B_TRANSLATE("HaikuDepot needs the package daemon to function, "
484 				"and it appears to be not running.\n"
485 				"Would you like to start it now?"),
486 			B_TRANSLATE("No, quit HaikuDepot"),
487 			B_TRANSLATE("Start package daemon"), NULL, B_WIDTH_AS_USUAL,
488 			B_WARNING_ALERT);
489 		alert->SetShortcut(0, B_ESCAPE);
490 
491 		if (alert->Go() == 0)
492 			exit(1);
493 
494 		if (!_LaunchPackageDaemon())
495 			break;
496 	}
497 }
498 
499 
500 bool
501 App::_LaunchPackageDaemon()
502 {
503 	status_t ret = be_roster->Launch(kPackageDaemonSignature);
504 	if (ret != B_OK) {
505 		BString errorMessage
506 			= B_TRANSLATE("Starting the package daemon failed:\n\n%Error%");
507 		errorMessage.ReplaceAll("%Error%", strerror(ret));
508 
509 		BAlert* alert = new BAlert(
510 			B_TRANSLATE("Package daemon problem"), errorMessage,
511 			B_TRANSLATE("Quit HaikuDepot"),
512 			B_TRANSLATE("Try again"), NULL, B_WIDTH_AS_USUAL,
513 			B_WARNING_ALERT);
514 		alert->SetShortcut(0, B_ESCAPE);
515 
516 		if (alert->Go() == 0)
517 			return false;
518 	}
519 	// TODO: Would be nice to send a message to the package daemon instead
520 	// and get a reply once it is ready.
521 	snooze(2000000);
522 	return true;
523 }
524 
525 
526 /*static*/ bool
527 App::_CheckIsFirstRun()
528 {
529 	BPath testFilePath;
530 	bool exists = false;
531 	status_t status = StorageUtils::LocalWorkingFilesPath("testfile.txt",
532 		testFilePath, false);
533 	if (status != B_OK)
534 		printf("! unable to establish the location of the test file\n");
535 	else
536 		status = StorageUtils::ExistsObject(testFilePath, &exists, NULL, NULL);
537 	return !exists;
538 }
539 
540 
541 /*! \brief Checks to ensure that a working file is able to be written.
542     \return false if the startup should be stopped and the application should
543             quit.
544 */
545 
546 bool
547 App::_CheckTestFile()
548 {
549 	BPath testFilePath;
550 	BString pathDescription = "???";
551 	status_t result = StorageUtils::LocalWorkingFilesPath("testfile.txt",
552 		testFilePath, false);
553 
554 	if (result == B_OK) {
555 		pathDescription = testFilePath.Path();
556 		result = StorageUtils::CheckCanWriteTo(testFilePath);
557 	}
558 
559 	if (result != B_OK) {
560 		StorageUtils::SetWorkingFilesUnavailable();
561 
562 		BString msg = B_TRANSLATE("This application writes and reads some"
563 			" working files on your computer in order to function. It appears"
564 			" that there are problems writing a test file at [%TestFilePath%]."
565 			" Check that there are no issues with your local disk or"
566 			" permissions that might prevent this application from writing"
567 			" files into that directory location. You may choose to acknowledge"
568 			" this problem and continue, but some functionality may be"
569 			" disabled.");
570 		msg.ReplaceAll("%TestFilePath%", pathDescription);
571 
572 		BAlert* alert = new(std::nothrow) BAlert(
573 			B_TRANSLATE("Problem with working files"),
574 			msg,
575 			B_TRANSLATE("Quit"), B_TRANSLATE("Continue"));
576 
577 		if (alert->Go() == 0)
578 			return false;
579 	}
580 
581 	return true;
582 }
583