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
41App::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
54App::~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
65bool
66App::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
82void
83App::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
103bool
104App::IsFirstRun()
105{
106	return fIsFirstRun;
107}
108
109
110void
111App::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
155void
156App::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
167enum 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
179static void
180app_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
204static arg_switch
205app_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
251void
252App::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
340void
341App::_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
367void
368App::_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
431void
432App::_ShowWindow(MainWindow* window)
433{
434	window->Show();
435	fWindowCount++;
436}
437
438
439bool
440App::_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
452void
453App::_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
482static const char* kPackageDaemonSignature
483	= "application/x-vnd.haiku-package_daemon";
484
485void
486App::_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
508bool
509App::_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
535App::_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
555bool
556App::_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
604void
605App::_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